From 633be8c2337c099a022eb57e375b563fdd6ca624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Murta?= Date: Mon, 7 Apr 2025 08:34:54 +0100 Subject: [PATCH] Add category filtering Thus implementing the category parameter on the /transactions path. Issue #2 --- internal/api/impl.go | 2 +- internal/api/impl_test.go | 18 ++++++++++++++-- internal/dal/dal.go | 2 +- internal/dal/impl.go | 15 +++++++++++--- internal/dal/impl_test.go | 39 ++++++++++++++++++++++++----------- internal/mock/mock_dal.gen.go | 8 +++---- 6 files changed, 61 insertions(+), 23 deletions(-) diff --git a/internal/api/impl.go b/internal/api/impl.go index b643c89..63468ac 100644 --- a/internal/api/impl.go +++ b/internal/api/impl.go @@ -59,7 +59,7 @@ func (server *ServerImpl) GetTransactions(ctx echo.Context, params GetTransactio offset = int(*params.Offset) } - transactions, err := server.Dal.Transactions(limit, offset) + transactions, err := server.Dal.Transactions(limit, offset, params.Category) if err != nil { return err } diff --git a/internal/api/impl_test.go b/internal/api/impl_test.go index e2b23ae..828efb7 100644 --- a/internal/api/impl_test.go +++ b/internal/api/impl_test.go @@ -146,6 +146,17 @@ func TestServerImpl_GetTransactions(t *testing.T) { false, entity.Transactions{{Id: 1, Date: time.Now(), Description: "desc#1", Value: decimal.New(0, 0)}}, }, + { + "200+category", + fields{mock_dal}, + args{GetTransactionsParams{ + Limit: golang.Ptr[int32](100), + Offset: golang.Ptr[int32](0), + Category: golang.Ptr("cat"), + }}, + false, + entity.Transactions{{Id: 1, Date: time.Now(), Description: "desc#1", Value: decimal.New(0, 0), Category: golang.Ptr("cat")}}, + }, { "204", fields{mock_dal}, @@ -164,13 +175,16 @@ func TestServerImpl_GetTransactions(t *testing.T) { rec := httptest.NewRecorder() ctx := echo.New().NewContext(req, rec) - mock_dal.EXPECT().Transactions(int(*tt.args.params.Limit), int(*tt.args.params.Offset)).Return(tt.mocks, nil).Times(1) + mock_dal.EXPECT(). + Transactions(int(*tt.args.params.Limit), int(*tt.args.params.Offset), tt.args.params.Category). + Return(tt.mocks, nil). + Times(1) if err := pf.GetTransactions(ctx, tt.args.params); (err != nil) != tt.wantErr { t.Errorf("ServerImpl.GetTransactions() error = %v,", err) } - code, _ := strconv.Atoi(tt.name) + code, _ := strconv.Atoi(strings.Split(tt.name, "+")[0]) assert.Equal(t, code, rec.Code) }) } diff --git a/internal/dal/dal.go b/internal/dal/dal.go index 7a3a5b9..ef9bd86 100644 --- a/internal/dal/dal.go +++ b/internal/dal/dal.go @@ -4,7 +4,7 @@ import "git.rosemyrtle.work/personal-finance/server/internal/entity" type DAL interface { Transaction(transactionId int64) (*entity.Transaction, error) - Transactions(limit, offset int) (entity.Transactions, error) + Transactions(limit, offset int, category *string) (entity.Transactions, error) InsertTransaction(entity.Transaction) (entity.Transaction, error) UpdateTransaction(entity.TransactionId, *entity.CategoryName) (bool, error) TransactionExists(uint64) (bool, error) diff --git a/internal/dal/impl.go b/internal/dal/impl.go index aa3b314..bcf64e7 100644 --- a/internal/dal/impl.go +++ b/internal/dal/impl.go @@ -41,8 +41,8 @@ func (dal *DalImpl) Transaction(transactionId int64) (*entity.Transaction, error return &transactions[0], nil } -func (dal *DalImpl) Transactions(limit, offset int) (entity.Transactions, error) { - log.Print("DAL::Transactions", "limit", limit, "offset", offset) +func (dal *DalImpl) Transactions(limit, offset int, category *string) (entity.Transactions, error) { + log.Print("DAL::Transactions", "limit", limit, "offset", offset, "category", category) if dal.Db == nil { log.Panic("database not available") @@ -53,12 +53,21 @@ func (dal *DalImpl) Transactions(limit, offset int) (entity.Transactions, error) FROM pfbudget.transactions t LEFT JOIN pfbudget.transactions_categorized tc ON t.id = tc.id + ` + args := []any{limit, offset} + + if category != nil { + stmt += `WHERE tc.name SIMILAR TO '%' || $3 || '%'` + args = append(args, *category) + } + stmt += ` ORDER BY t.date DESC LIMIT $1 OFFSET $2 ` + log.Print(stmt, args) - rows, err := dal.Db.Query(stmt, limit, offset) + rows, err := dal.Db.Query(stmt, args...) if err != nil { return entity.Transactions{}, err } diff --git a/internal/dal/impl_test.go b/internal/dal/impl_test.go index 9a4ec5a..5435ccb 100644 --- a/internal/dal/impl_test.go +++ b/internal/dal/impl_test.go @@ -99,9 +99,14 @@ func TestDalImpl_Transactions(t *testing.T) { type fields struct { Db *sql.DB } + type args struct { + limit, offset int + category *string + } tests := []struct { name string fields fields + args args mocks [][]driver.Value want entity.Transactions wantErr bool @@ -109,6 +114,7 @@ func TestDalImpl_Transactions(t *testing.T) { { "SelectTransactions", fields{db}, + args{limit: 30, offset: 0, category: nil}, [][]driver.Value{ {1, date, "income", 1000, nil}, {2, date, "expense", -10.50, nil}, @@ -122,6 +128,7 @@ func TestDalImpl_Transactions(t *testing.T) { { "SelectTransactionsWithCategory", fields{db}, + args{limit: 30, offset: 0, category: golang.Ptr("C1")}, [][]driver.Value{ {1, date, "income", 1000, "C1"}, {2, date, "expense", -10.50, nil}, @@ -133,7 +140,7 @@ func TestDalImpl_Transactions(t *testing.T) { false, }, { - "SelectNoTransactions", fields{db}, nil, nil, false, + "SelectNoTransactions", fields{db}, args{limit: 30, offset: 0, category: nil}, nil, nil, false, }, } for _, tt := range tests { @@ -142,23 +149,31 @@ func TestDalImpl_Transactions(t *testing.T) { Db: tt.fields.Db, } - limit, offset := 0, 0 + args := []driver.Value{tt.args.limit, tt.args.offset} + stmt := ` + ^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name + FROM \w+\.transactions \w+ + LEFT JOIN \w+\.transactions_categorized \w+ + ON \w+\.id = \w+\.id + ` + if tt.args.category != nil { + stmt += `WHERE \w+\.name SIMILAR TO '%' || \$3 || '%'` + args = append(args, *tt.args.category) + } + stmt += ` + ORDER BY \w+\.date DESC + LIMIT \$1 + OFFSET \$2 + ` mock. - ExpectQuery(` - ^SELECT \w+\.id, \w+\.date, \w+\.description, \w+\.amount, \w+\.name - FROM \w+\.transactions \w+ - LEFT JOIN \w+\.transactions_categorized \w+ - ON \w+\.id = \w+\.id - ORDER BY \w+\.date DESC - LIMIT \$1 - OFFSET \$2`). - WithArgs(limit, offset). + ExpectQuery(stmt). + WithArgs(args...). WillReturnRows( mock.NewRows([]string{"id", "date", "description", "amount", "category"}).AddRows(tt.mocks...), ) - got, err := dal.Transactions(limit, offset) + got, err := dal.Transactions(tt.args.limit, tt.args.offset, tt.args.category) if (err != nil) != tt.wantErr { t.Errorf("DalImpl.Transactions() error = %v, wantErr %v", err, tt.wantErr) return diff --git a/internal/mock/mock_dal.gen.go b/internal/mock/mock_dal.gen.go index c43f06b..582c274 100644 --- a/internal/mock/mock_dal.gen.go +++ b/internal/mock/mock_dal.gen.go @@ -131,18 +131,18 @@ func (mr *MockDALMockRecorder) TransactionExists(arg0 any) *gomock.Call { } // Transactions mocks base method. -func (m *MockDAL) Transactions(limit, offset int) ([]entity.Transaction, error) { +func (m *MockDAL) Transactions(limit, offset int, category *string) ([]entity.Transaction, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Transactions", limit, offset) + ret := m.ctrl.Call(m, "Transactions", limit, offset, category) ret0, _ := ret[0].([]entity.Transaction) ret1, _ := ret[1].(error) return ret0, ret1 } // Transactions indicates an expected call of Transactions. -func (mr *MockDALMockRecorder) Transactions(limit, offset any) *gomock.Call { +func (mr *MockDALMockRecorder) Transactions(limit, offset, category any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transactions", reflect.TypeOf((*MockDAL)(nil).Transactions), limit, offset) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Transactions", reflect.TypeOf((*MockDAL)(nil).Transactions), limit, offset, category) } // UpdateTransaction mocks base method.