Skip to content

Commit 8160786

Browse files
authored
feat: filter by all ancestors (#1015)
1 parent 5b41236 commit 8160786

File tree

9 files changed

+129
-41
lines changed

9 files changed

+129
-41
lines changed

pkg/controllers/v4/envelope.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package v4
22

33
import (
4+
"fmt"
45
"net/http"
56

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

144145
q := models.DB.
145-
Order("name ASC").
146+
Order("envelopes.name ASC").
146147
Where(&model, queryFields...)
147148

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

151+
if filter.BudgetID != "" {
152+
budgetID, err := httputil.UUIDFromString(filter.BudgetID)
153+
if err != nil {
154+
s := fmt.Sprintf("Error parsing budget ID for filtering: %s", err.Error())
155+
c.JSON(status(err), EnvelopeListResponse{
156+
Error: &s,
157+
})
158+
return
159+
}
160+
161+
q = q.
162+
Joins("JOIN categories on categories.id = envelopes.category_id").
163+
Joins("JOIN budgets on budgets.id = categories.budget_id").
164+
Where("budgets.id = ?", budgetID)
165+
}
166+
150167
// Set the offset. Does not need checking since the default is 0
151168
q = q.Offset(int(filter.Offset))
152169

pkg/controllers/v4/envelope_test.go

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,11 @@ func (suite *TestSuiteStandard) TestEnvelopesGetSingle() {
142142
}
143143

144144
func (suite *TestSuiteStandard) TestEnvelopesGetFilter() {
145-
c1 := createTestCategory(suite.T(), v4.CategoryEditable{})
146-
c2 := createTestCategory(suite.T(), v4.CategoryEditable{})
145+
b1 := createTestBudget(suite.T(), v4.BudgetEditable{})
146+
b2 := createTestBudget(suite.T(), v4.BudgetEditable{})
147+
148+
c1 := createTestCategory(suite.T(), v4.CategoryEditable{BudgetID: b1.Data.ID})
149+
c2 := createTestCategory(suite.T(), v4.CategoryEditable{BudgetID: b2.Data.ID})
147150

148151
_ = createTestEnvelope(suite.T(), v4.EnvelopeEditable{
149152
Name: "Groceries",
@@ -170,38 +173,41 @@ func (suite *TestSuiteStandard) TestEnvelopesGetFilter() {
170173
len int
171174
checkFunc func(t *testing.T, envelopes []v4.Envelope)
172175
}{
176+
{"Archived", "archived=true", 1, func(t *testing.T, envelopes []v4.Envelope) {
177+
for _, e := range envelopes {
178+
assert.True(t, e.Archived)
179+
}
180+
}},
181+
{"Budget 1", fmt.Sprintf("budget=%s", b1.Data.ID), 1, nil},
182+
{"Budget 2", fmt.Sprintf("budget=%s", b2.Data.ID), 2, nil},
173183
{"Category 2", fmt.Sprintf("category=%s", c2.Data.ID), 2, nil},
174184
{"Category Not Existing", "category=e0f9ff7a-9f07-463c-bbd2-0d72d09d3cc6", 0, nil},
175-
{"Empty Note", "note=", 0, nil},
176185
{"Empty Name", "name=", 0, nil},
177-
{"Name & Note", "name=Groceries&note=For the stuff bought in supermarkets", 1, nil},
186+
{"Empty Note", "note=", 0, nil},
178187
{"Fuzzy name", "name=es", 2, nil},
179188
{"Fuzzy note", "note=Because", 2, nil},
189+
{"Limit -1", "limit=-1", 3, nil},
190+
{"Limit 0", "limit=0", 0, nil},
191+
{"Limit 4", "limit=4", 3, nil},
192+
{"Offset 0, limit 2", "offset=0&limit=2", 2, nil},
193+
{"Name & Note", "name=Groceries&note=For the stuff bought in supermarkets", 1, nil},
194+
{"Non-matching budget", fmt.Sprintf("budget=%s", uuid.New()), 0, nil},
180195
{"Not archived", "archived=false", 2, func(t *testing.T, envelopes []v4.Envelope) {
181196
for _, e := range envelopes {
182197
assert.False(t, e.Archived)
183198
}
184199
}},
185-
{"Archived", "archived=true", 1, func(t *testing.T, envelopes []v4.Envelope) {
186-
for _, e := range envelopes {
187-
assert.True(t, e.Archived)
188-
}
189-
}},
200+
{"Offset 2", "offset=2", 1, nil},
190201
{"Search for 'hair'", "search=hair", 2, nil},
191202
{"Search for 'st'", "search=st", 2, nil},
192203
{"Search for 'STUFF'", "search=STUFF", 1, nil},
193-
{"Offset 2", "offset=2", 1, nil},
194-
{"Offset 0, limit 2", "offset=0&limit=2", 2, nil},
195-
{"Limit 4", "limit=4", 3, nil},
196-
{"Limit 0", "limit=0", 0, nil},
197-
{"Limit -1", "limit=-1", 3, nil},
198204
}
199205

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

207213
assert.Equal(t, tt.len, len(re.Data), "Request ID: %s", r.Result().Header.Get("x-request-id"))

pkg/controllers/v4/envelope_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ type EnvelopeResponse struct {
8989
}
9090

9191
type EnvelopeQueryFilter struct {
92+
BudgetID string `form:"budget" filterField:"false"` // By budget ID
9293
CategoryID string `form:"category"` // By the ID of the category
9394
Name string `form:"name" filterField:"false"` // By name
9495
Note string `form:"note" filterField:"false"` // By the note

pkg/controllers/v4/goal.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package v4
22

33
import (
4+
"fmt"
45
"net/http"
56

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

151152
q := models.DB.
152-
Order("date(month) ASC, name ASC").
153+
Order("date(goals.month) ASC, goals.name ASC").
153154
Where(&where, queryFields...)
154155

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

202+
if filter.CategoryID != "" {
203+
categoryID, err := httputil.UUIDFromString(filter.CategoryID)
204+
if err != nil {
205+
s := fmt.Sprintf("Error parsing category ID for filtering: %s", err.Error())
206+
c.JSON(status(err), GoalListResponse{
207+
Error: &s,
208+
})
209+
return
210+
}
211+
212+
q = q.
213+
Joins("JOIN envelopes AS category_filter_envelopes on category_filter_envelopes.id = goals.envelope_id").
214+
Joins("JOIN categories AS category_filter_categories on category_filter_categories.id = category_filter_envelopes.category_id").
215+
Where("category_filter_categories.id = ?", categoryID)
216+
}
217+
218+
if filter.BudgetID != "" {
219+
budgetID, err := httputil.UUIDFromString(filter.BudgetID)
220+
if err != nil {
221+
s := fmt.Sprintf("Error parsing budget ID for filtering: %s", err.Error())
222+
c.JSON(status(err), GoalListResponse{
223+
Error: &s,
224+
})
225+
return
226+
}
227+
228+
q = q.
229+
Joins("JOIN envelopes on envelopes.id = goals.envelope_id").
230+
Joins("JOIN categories on categories.id = envelopes.category_id").
231+
Joins("JOIN budgets on budgets.id = categories.budget_id").
232+
Where("budgets.id = ?", budgetID)
233+
}
234+
201235
var goals []models.Goal
202236
err = q.Find(&goals).Error
203237
if err != nil {

pkg/controllers/v4/goal_test.go

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,11 @@ func (suite *TestSuiteStandard) TestGoalsGet() {
158158
func (suite *TestSuiteStandard) TestGoalsGetFilter() {
159159
b := createTestBudget(suite.T(), v4.BudgetEditable{})
160160

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

163-
e1 := createTestEnvelope(suite.T(), v4.EnvelopeEditable{CategoryID: c.Data.ID})
164-
e2 := createTestEnvelope(suite.T(), v4.EnvelopeEditable{CategoryID: c.Data.ID})
164+
e1 := createTestEnvelope(suite.T(), v4.EnvelopeEditable{CategoryID: c1.Data.ID})
165+
e2 := createTestEnvelope(suite.T(), v4.EnvelopeEditable{CategoryID: c2.Data.ID})
165166

166167
_ = createTestGoal(suite.T(), v4.GoalEditable{
167168
Name: "Test Goal",
@@ -194,33 +195,39 @@ func (suite *TestSuiteStandard) TestGoalsGetFilter() {
194195
query string
195196
len int
196197
}{
197-
{"Same month", fmt.Sprintf("month=%s", types.NewMonth(2024, 1)), 2},
198-
{"After month", fmt.Sprintf("fromMonth=%s", types.NewMonth(2024, 2)), 1},
199-
{"Before month", fmt.Sprintf("untilMonth=%s", types.NewMonth(2024, 2)), 3},
200198
{"After all months", fmt.Sprintf("fromMonth=%s", types.NewMonth(2024, 6)), 0},
201-
{"Before all months", fmt.Sprintf("untilMonth=%s", types.NewMonth(2023, 6)), 0},
202-
{"Impossible between two months", fmt.Sprintf("fromMonth=%s&untilMonth=%s", types.NewMonth(2024, 11), types.NewMonth(2024, 10)), 0},
203-
{"Exact Amount", fmt.Sprintf("amount=%s", decimal.NewFromFloat(200).String()), 1},
204-
{"Note", "note=can", 1},
205-
{"No note", "note=", 1},
206-
{"Fuzzy note", "note=so", 2},
199+
{"After month", fmt.Sprintf("fromMonth=%s", types.NewMonth(2024, 2)), 1},
207200
{"Amount less or equal to 99", "amountLessOrEqual=99", 0},
208201
{"Amount less or equal to 200", "amountLessOrEqual=200", 2},
209202
{"Amount more or equal to 3", "amountMoreOrEqual=3", 3},
203+
{"Amount more or equal to 50 and less than 500", "amountMoreOrEqual=50&amountLessOrEqual=500", 2},
204+
{"Amount more or equal to 100 and less than 10", "amountMoreOrEqual=100&amountLessOrEqual=10", 0},
210205
{"Amount more or equal to 500.813", "amountMoreOrEqual=500.813", 1},
211206
{"Amount more or equal to 99999", "amountMoreOrEqual=99999", 0},
212-
{"Amount more or equal to 100 and less than 10", "amountMoreOrEqual=100&amountLessOrEqual=10", 0},
213-
{"Amount more or equal to 50 and less than 500", "amountMoreOrEqual=50&amountLessOrEqual=500", 2},
207+
{"Before all months", fmt.Sprintf("untilMonth=%s", types.NewMonth(2023, 6)), 0},
208+
{"Before month", fmt.Sprintf("untilMonth=%s", types.NewMonth(2024, 2)), 3},
209+
{"Budget matches", fmt.Sprintf("budget=%s", b.Data.ID), 3},
210+
{"Budget does not match", fmt.Sprintf("budget=%s", uuid.New()), 0},
211+
{"Category 1", fmt.Sprintf("category=%s", c1.Data.ID), 2},
212+
{"Category 1, but budget does not match", fmt.Sprintf("category=%s&budget=%s", c1.Data.ID, uuid.New()), 0},
213+
{"Category 2", fmt.Sprintf("category=%s", c2.Data.ID), 1},
214+
{"Category does not match", fmt.Sprintf("category=%s", uuid.New()), 0},
215+
{"Exact Amount", fmt.Sprintf("amount=%s", decimal.NewFromFloat(200).String()), 1},
216+
{"Fuzzy note", "note=so", 2},
217+
{"Impossible between two months", fmt.Sprintf("fromMonth=%s&untilMonth=%s", types.NewMonth(2024, 11), types.NewMonth(2024, 10)), 0},
218+
{"Limit and Fuzzy Note", "limit=1&note=so", 1},
219+
{"Limit and Offset", "limit=1&offset=1", 1},
220+
{"Limit negative", "limit=-123", 3},
214221
{"Limit positive", "limit=2", 2},
215-
{"Limit zero", "limit=0", 0},
216222
{"Limit unset", "limit=-1", 3},
217-
{"Limit negative", "limit=-123", 3},
218-
{"Offset zero", "offset=0", 3},
219-
{"Offset positive", "offset=2", 1},
220-
{"Offset higher than number", "offset=5", 0},
221-
{"Limit and Offset", "limit=1&offset=1", 1},
222-
{"Limit and Fuzzy Note", "limit=1&note=so", 1},
223+
{"Limit zero", "limit=0", 0},
224+
{"No note", "note=", 1},
225+
{"Note", "note=can", 1},
223226
{"Offset and Fuzzy Note", "offset=2&note=they", 0},
227+
{"Offset higher than number", "offset=5", 0},
228+
{"Offset positive", "offset=2", 1},
229+
{"Offset zero", "offset=0", 3},
230+
{"Same month", fmt.Sprintf("month=%s", types.NewMonth(2024, 1)), 2},
224231
}
225232

226233
for _, tt := range tests {

pkg/controllers/v4/goal_types.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ type GoalResponse struct {
9494
}
9595

9696
type GoalQueryFilter struct {
97+
BudgetID string `form:"budget" filterField:"false"` // By budget ID
98+
CategoryID string `form:"category" filterField:"false"` // By category ID
9799
Name string `form:"name" filterField:"false"` // By name
98100
Note string `form:"note" filterField:"false"` // By the note
99101
Search string `form:"search" filterField:"false"` // By string in name or note

pkg/controllers/v4/match_rule.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,22 @@ func GetMatchRules(c *gin.Context) {
151151
q = q.Where("match = ''")
152152
}
153153

154+
if filter.BudgetID != "" {
155+
budgetID, err := httputil.UUIDFromString(filter.BudgetID)
156+
if err != nil {
157+
s := fmt.Sprintf("Error parsing budget ID for filtering: %s", err.Error())
158+
c.JSON(status(err), MatchRuleListResponse{
159+
Error: &s,
160+
})
161+
return
162+
}
163+
164+
q = q.
165+
Joins("JOIN accounts on accounts.id = match_rules.account_id").
166+
Joins("JOIN budgets on budgets.id = accounts.budget_id").
167+
Where("budgets.id = ?", budgetID)
168+
}
169+
154170
// Set the offset. Does not need checking since the default is 0
155171
q = q.Offset(int(filter.Offset))
156172

pkg/controllers/v4/match_rule_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,11 @@ func (suite *TestSuiteStandard) TestMatchRulesDatabaseError() {
187187

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

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

195196
_ = createTestMatchRule(suite.T(), v4.MatchRuleEditable{
196197
Priority: 1,
@@ -215,6 +216,9 @@ func (suite *TestSuiteStandard) TestMatchRulesGetFilter() {
215216
query string
216217
len int
217218
}{
219+
{"Budget 1", fmt.Sprintf("budget=%s", b1.Data.ID), 1},
220+
{"Budget 2", fmt.Sprintf("budget=%s", b2.Data.ID), 2},
221+
{"Budget does not match", fmt.Sprintf("budget=%s", uuid.New()), 0},
218222
{"Limit over count", "limit=5", 3},
219223
{"Limit under count", "limit=2", 2},
220224
{"Offset", "offset=2", 1},

pkg/controllers/v4/match_rule_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ func newMatchRule(c *gin.Context, model models.MatchRule) MatchRule {
8181

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

0 commit comments

Comments
 (0)