From c1b6efa8d4edebd65014c5ccdedb91a7ff09905e Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Fri, 25 Apr 2025 21:09:12 +0800 Subject: [PATCH 1/5] Implement transaction repository and service with caching; update related handlers and tests --- cmd/server/providers.go | 13 +++- cmd/server/wire.go | 2 + cmd/server/wire_gen.go | 4 +- internal/infrastructure/auth/dummy.go | 9 ++- .../persistence/mongodb/transaction.go | 3 +- .../persistence/mongodb/transaction_test.go | 2 +- .../persistence/redis/transaction_test.go | 20 ++--- internal/interfaces/http/report.go | 18 ++--- internal/interfaces/http/transaction.go | 2 +- internal/interfaces/http/transaction_test.go | 18 ++--- .../transaction/repository/repository.go | 4 +- .../module/transaction/usecase/service.go | 76 ++++++++++++++++++- 12 files changed, 131 insertions(+), 40 deletions(-) diff --git a/cmd/server/providers.go b/cmd/server/providers.go index 90afc8c..f80d756 100644 --- a/cmd/server/providers.go +++ b/cmd/server/providers.go @@ -20,6 +20,7 @@ import ( goal_usecase "github.com/Financial-Partner/server/internal/module/goal/usecase" investment_usecase "github.com/Financial-Partner/server/internal/module/investment/usecase" report_usecase "github.com/Financial-Partner/server/internal/module/report/usecase" + transaction_repository "github.com/Financial-Partner/server/internal/module/transaction/repository" transaction_usecase "github.com/Financial-Partner/server/internal/module/transaction/usecase" user_repository "github.com/Financial-Partner/server/internal/module/user/repository" user_usecase "github.com/Financial-Partner/server/internal/module/user/usecase" @@ -58,6 +59,14 @@ func ProvideUserService(repo user_repository.Repository, store *perRedis.UserSto return user_usecase.NewService(repo, store, log) } +func ProvideTransactionRepository(db *dbInfra.Client) transaction_repository.Repository { + return perMongo.NewTransactionRepository(db) +} + +func ProvideTransactionStore(cache *cacheInfra.Client) *perRedis.TransactionStore { + return perRedis.NewTransactionStore(cache) +} + func ProvideJWTManager(cfg *config.Config) *authInfra.JWTManager { return authInfra.NewJWTManager(cfg.JWT.SecretKey, cfg.JWT.AccessExpiry, cfg.JWT.RefreshExpiry) } @@ -84,8 +93,8 @@ func ProvideInvestmentService() *investment_usecase.Service { return investment_usecase.NewService() } -func ProvideTransactionService() *transaction_usecase.Service { - return transaction_usecase.NewService() +func ProvideTransactionService(repo transaction_repository.Repository, store *perRedis.TransactionStore, log loggerInfra.Logger) *transaction_usecase.Service { + return transaction_usecase.NewService(repo, store, log) } func ProvideGachaService() *gacha_usecase.Service { diff --git a/cmd/server/wire.go b/cmd/server/wire.go index 6cd6159..a899b8f 100644 --- a/cmd/server/wire.go +++ b/cmd/server/wire.go @@ -23,6 +23,8 @@ func InitializeServer(cfgFile string) (*Server, error) { ProvideAuthService, ProvideGoalService, ProvideInvestmentService, + ProvideTransactionRepository, + ProvideTransactionStore, ProvideTransactionService, ProvideGachaService, ProvideReportService, diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index 3ecdfe9..6cc36c3 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -38,7 +38,9 @@ func InitializeServer(cfgFile string) (*Server, error) { auth_usecaseService := ProvideAuthService(config, authClient, jwtManager, tokenStore, service) goal_usecaseService := ProvideGoalService() investment_usecaseService := ProvideInvestmentService() - transaction_usecaseService := ProvideTransactionService() + transaction_repositoryRepository := ProvideTransactionRepository(client) + transactionStore := ProvideTransactionStore(cacheClient) + transaction_usecaseService := ProvideTransactionService(transaction_repositoryRepository, transactionStore, logger) gacha_usecaseService := ProvideGachaService() report_usecaseService := ProvideReportService() handler := ProvideHandler(service, auth_usecaseService, goal_usecaseService, investment_usecaseService, transaction_usecaseService, gacha_usecaseService, report_usecaseService, logger) diff --git a/internal/infrastructure/auth/dummy.go b/internal/infrastructure/auth/dummy.go index 3b83d87..aeb7e77 100644 --- a/internal/infrastructure/auth/dummy.go +++ b/internal/infrastructure/auth/dummy.go @@ -3,6 +3,8 @@ package auth import ( "errors" + "go.mongodb.org/mongo-driver/bson/primitive" + "github.com/Financial-Partner/server/internal/config" ) @@ -21,8 +23,13 @@ func (v *DummyJWTValidator) ValidateToken(tokenString string) (*Claims, error) { return nil, errors.New("invalid token") } + dummyObjectID, err := primitive.ObjectIDFromHex("680b4fc122fc6fd9212d78f9") + if err != nil { + return nil, errors.New("failed to create dummy ObjectID") + } + return &Claims{ - ID: "test-id", + ID: dummyObjectID.Hex(), Email: "bypass@example.com", }, nil } diff --git a/internal/infrastructure/persistence/mongodb/transaction.go b/internal/infrastructure/persistence/mongodb/transaction.go index dbb2efc..a4ce4a2 100644 --- a/internal/infrastructure/persistence/mongodb/transaction.go +++ b/internal/infrastructure/persistence/mongodb/transaction.go @@ -4,6 +4,7 @@ import ( "context" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "github.com/Financial-Partner/server/internal/entities" @@ -28,7 +29,7 @@ func (r *MongoTransactionRepository) Create(ctx context.Context, entity *entitie return entity, nil } -func (r *MongoTransactionRepository) FindByUserId(ctx context.Context, userID string) ([]entities.Transaction, error) { +func (r *MongoTransactionRepository) FindByUserId(ctx context.Context, userID primitive.ObjectID) ([]entities.Transaction, error) { var transactions []entities.Transaction cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID}) if err != nil { diff --git a/internal/infrastructure/persistence/mongodb/transaction_test.go b/internal/infrastructure/persistence/mongodb/transaction_test.go index accfde9..9363c3a 100644 --- a/internal/infrastructure/persistence/mongodb/transaction_test.go +++ b/internal/infrastructure/persistence/mongodb/transaction_test.go @@ -17,7 +17,7 @@ import ( func TestMongoTransactionRepository(t *testing.T) { mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock)) - testUserID := primitive.NewObjectID().Hex() + testUserID := primitive.NewObjectID() testTransactions := []entities.Transaction{ { ID: primitive.NewObjectID(), diff --git a/internal/infrastructure/persistence/redis/transaction_test.go b/internal/infrastructure/persistence/redis/transaction_test.go index 6e9e6fe..be55c79 100644 --- a/internal/infrastructure/persistence/redis/transaction_test.go +++ b/internal/infrastructure/persistence/redis/transaction_test.go @@ -25,7 +25,7 @@ func TestTransactionStore(t *testing.T) { transactionStore := redis.NewTransactionStore(mockRedisClient) // Mock data - userID := primitive.NewObjectID().Hex() + userID := primitive.NewObjectID() mockTransactions := []entities.Transaction{ { @@ -56,12 +56,12 @@ func TestTransactionStore(t *testing.T) { mockData, _ := json.Marshal(mockTransactions) // Mock the Get method to return the serialized JSON data - mockRedisClient.EXPECT().Get(gomock.Any(), fmt.Sprintf("user:%s:transactions", userID), gomock.Any()).DoAndReturn( + mockRedisClient.EXPECT().Get(gomock.Any(), fmt.Sprintf("user:%s:transactions", userID.Hex()), gomock.Any()).DoAndReturn( func(_ context.Context, _ string, dest interface{}) error { return json.Unmarshal(mockData, dest) }, ) - transactions, err := transactionStore.GetByUserId(context.Background(), userID) + transactions, err := transactionStore.GetByUserId(context.Background(), userID.Hex()) require.NoError(t, err) assert.NotNil(t, transactions) }) @@ -70,10 +70,10 @@ func TestTransactionStore(t *testing.T) { mockRedisClient := redis.NewMockRedisClient(ctrl) transactionStore := redis.NewTransactionStore(mockRedisClient) - userID := primitive.NewObjectID().Hex() + userID := primitive.NewObjectID() mockRedisClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(goredis.Nil) - transactions, err := transactionStore.GetByUserId(context.Background(), userID) + transactions, err := transactionStore.GetByUserId(context.Background(), userID.Hex()) require.Error(t, err) assert.Nil(t, transactions) }) @@ -83,7 +83,7 @@ func TestTransactionStore(t *testing.T) { transactionStore := redis.NewTransactionStore(mockRedisClient) // mock data - userID := primitive.NewObjectID().Hex() + userID := primitive.NewObjectID() mockTransactions := []entities.Transaction{ { ID: primitive.NewObjectID(), @@ -110,7 +110,7 @@ func TestTransactionStore(t *testing.T) { } mockRedisClient.EXPECT().Set(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - err := transactionStore.SetByUserId(context.Background(), userID, mockTransactions) + err := transactionStore.SetByUserId(context.Background(), userID.Hex(), mockTransactions) require.NoError(t, err) }) @@ -118,10 +118,10 @@ func TestTransactionStore(t *testing.T) { mockRedisClient := redis.NewMockRedisClient(ctrl) transactionStore := redis.NewTransactionStore(mockRedisClient) - userID := primitive.NewObjectID().Hex() - mockRedisClient.EXPECT().Delete(gomock.Any(), fmt.Sprintf("user:%s:transactions", userID)).Return(nil) + userID := primitive.NewObjectID() + mockRedisClient.EXPECT().Delete(gomock.Any(), fmt.Sprintf("user:%s:transactions", userID.Hex())).Return(nil) - err := transactionStore.DeleteByUserId(context.Background(), userID) + err := transactionStore.DeleteByUserId(context.Background(), userID.Hex()) require.NoError(t, err) }) } diff --git a/internal/interfaces/http/report.go b/internal/interfaces/http/report.go index c5a2cd4..8eb6932 100644 --- a/internal/interfaces/http/report.go +++ b/internal/interfaces/http/report.go @@ -10,7 +10,7 @@ import ( "github.com/Financial-Partner/server/internal/entities" "github.com/Financial-Partner/server/internal/interfaces/http/dto" httperror "github.com/Financial-Partner/server/internal/interfaces/http/error" - responde "github.com/Financial-Partner/server/internal/interfaces/http/respond" + respond "github.com/Financial-Partner/server/internal/interfaces/http/respond" ) //go:generate mockgen -source=report.go -destination=report_mock.go -package=handler @@ -46,7 +46,7 @@ func (h *Handler) GetReport(w http.ResponseWriter, r *http.Request) { startTimestamp, err := strconv.ParseInt(start, 10, 64) // Parse Unix timestamp if err != nil { h.log.Warnf("Invalid start timestamp format. Use a valid Unix timestamp.") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidParameter, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidParameter, http.StatusBadRequest) return } startDate = time.Unix(startTimestamp, 0).UTC() // Convert to time.Time in UTC @@ -56,7 +56,7 @@ func (h *Handler) GetReport(w http.ResponseWriter, r *http.Request) { endTimestamp, err := strconv.ParseInt(end, 10, 64) // Parse Unix timestamp if err != nil { h.log.Warnf("Invalid end timestamp format. Use a valid Unix timestamp.") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidParameter, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidParameter, http.StatusBadRequest) return } endDate = time.Unix(endTimestamp, 0).UTC() // Convert to time.Time in UTC @@ -65,14 +65,14 @@ func (h *Handler) GetReport(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } report, err := h.reportService.GetReport(r.Context(), userID, startDate, endDate, reportType) if err != nil { h.log.Errorf("failed to get report") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetReport, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetReport, http.StatusInternalServerError) return } @@ -85,7 +85,7 @@ func (h *Handler) GetReport(w http.ResponseWriter, r *http.Request) { Percentages: report.Percentages, } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } // @Summary Get report summary @@ -102,14 +102,14 @@ func (h *Handler) GetReportSummary(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } reportSummary, err := h.reportService.GetReportSummary(r.Context(), userID) if err != nil { h.log.Errorf("failed to get report summary") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetReportSummary, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetReportSummary, http.StatusInternalServerError) return } @@ -117,5 +117,5 @@ func (h *Handler) GetReportSummary(w http.ResponseWriter, r *http.Request) { Summary: reportSummary.Summary, } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } diff --git a/internal/interfaces/http/transaction.go b/internal/interfaces/http/transaction.go index 9d1453b..4e56727 100644 --- a/internal/interfaces/http/transaction.go +++ b/internal/interfaces/http/transaction.go @@ -94,7 +94,7 @@ func (h *Handler) CreateTransaction(w http.ResponseWriter, r *http.Request) { transaction, err := h.transactionService.CreateTransaction(r.Context(), userID, &req) if err != nil { - h.log.Errorf("failed to create transaction") + h.log.Errorf("failed to create transaction: %v", err) respond.WithError(w, r, h.log, err, httperror.ErrFailedToCreateTransaction, http.StatusInternalServerError) return } diff --git a/internal/interfaces/http/transaction_test.go b/internal/interfaces/http/transaction_test.go index 57d8ae9..3e529f5 100644 --- a/internal/interfaces/http/transaction_test.go +++ b/internal/interfaces/http/transaction_test.go @@ -110,7 +110,7 @@ func TestCreateTransaction(t *testing.T) { t.Run("Success", func(t *testing.T) { h, mockServices := newTestHandler(t) - userID := primitive.NewObjectID().Hex() + userID := primitive.NewObjectID() userEmail := "test@example.com" now := time.Now() @@ -128,7 +128,7 @@ func TestCreateTransaction(t *testing.T) { } mockServices.TransactionService.EXPECT(). - CreateTransaction(gomock.Any(), userID, gomock.Any()). + CreateTransaction(gomock.Any(), userID.Hex(), gomock.Any()). Return(transaction, nil) req := dto.CreateTransactionRequest{ @@ -141,7 +141,7 @@ func TestCreateTransaction(t *testing.T) { body, _ := json.Marshal(req) w := httptest.NewRecorder() r := httptest.NewRequest("POST", "/transactions", bytes.NewBuffer(body)) - ctx := newContext(userID, userEmail) + ctx := newContext(userID.Hex(), userEmail) r = r.WithContext(ctx) h.CreateTransaction(w, r) @@ -187,16 +187,16 @@ func TestGetTransactions(t *testing.T) { t.Run("Service error", func(t *testing.T) { h, mockServices := newTestHandler(t) - userID := primitive.NewObjectID().Hex() + userID := primitive.NewObjectID() userEmail := "test@example.com" mockServices.TransactionService.EXPECT(). - GetTransactions(gomock.Any(), userID). + GetTransactions(gomock.Any(), userID.Hex()). Return(nil, errors.New("service error")) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/transactions", nil) - ctx := newContext(userID, userEmail) + ctx := newContext(userID.Hex(), userEmail) r = r.WithContext(ctx) h.GetTransactions(w, r) @@ -214,7 +214,7 @@ func TestGetTransactions(t *testing.T) { t.Run("Success", func(t *testing.T) { h, mockServices := newTestHandler(t) - userID := primitive.NewObjectID().Hex() + userID := primitive.NewObjectID() userEmail := "test@example.com" now := time.Now() @@ -234,12 +234,12 @@ func TestGetTransactions(t *testing.T) { } mockServices.TransactionService.EXPECT(). - GetTransactions(gomock.Any(), userID). + GetTransactions(gomock.Any(), userID.Hex()). Return(transactions, nil) w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/transactions", nil) - ctx := newContext(userID, userEmail) + ctx := newContext(userID.Hex(), userEmail) r = r.WithContext(ctx) h.GetTransactions(w, r) diff --git a/internal/module/transaction/repository/repository.go b/internal/module/transaction/repository/repository.go index 99d18f0..49ed31f 100644 --- a/internal/module/transaction/repository/repository.go +++ b/internal/module/transaction/repository/repository.go @@ -3,6 +3,8 @@ package transaction_repository import ( "context" + "go.mongodb.org/mongo-driver/bson/primitive" + "github.com/Financial-Partner/server/internal/entities" ) @@ -10,7 +12,7 @@ import ( type Repository interface { Create(ctx context.Context, transaction *entities.Transaction) (*entities.Transaction, error) - FindByUserId(ctx context.Context, userID string) ([]entities.Transaction, error) + FindByUserId(ctx context.Context, userID primitive.ObjectID) ([]entities.Transaction, error) } type TransactionStore interface { diff --git a/internal/module/transaction/usecase/service.go b/internal/module/transaction/usecase/service.go index f4fd091..31761bf 100644 --- a/internal/module/transaction/usecase/service.go +++ b/internal/module/transaction/usecase/service.go @@ -2,22 +2,90 @@ package transaction_usecase import ( "context" + "fmt" + "time" "github.com/Financial-Partner/server/internal/entities" + "github.com/Financial-Partner/server/internal/infrastructure/logger" "github.com/Financial-Partner/server/internal/interfaces/http/dto" + transaction_repository "github.com/Financial-Partner/server/internal/module/transaction/repository" + "go.mongodb.org/mongo-driver/bson/primitive" ) type Service struct { + repo transaction_repository.Repository + store transaction_repository.TransactionStore + log logger.Logger } -func NewService() *Service { - return &Service{} +func NewService(repo transaction_repository.Repository, store transaction_repository.TransactionStore, log logger.Logger) *Service { + return &Service{ + repo: repo, + store: store, + log: log, + } } func (s *Service) CreateTransaction(ctx context.Context, userID string, req *dto.CreateTransactionRequest) (*entities.Transaction, error) { - return nil, nil + transactionDate, err := time.Parse("2006-01-02", req.Date) + if err != nil { + return nil, fmt.Errorf("invalid date format: %w", err) + } + + objectID, err := primitive.ObjectIDFromHex(userID) + if err != nil { + return nil, fmt.Errorf("invalid user ID: %w", err) + } + + // Convert DTO to Entity + transaction := &entities.Transaction{ + UserID: objectID, + Amount: req.Amount, + Category: req.Category, + Type: req.Type, + Date: transactionDate.UTC(), + Description: req.Description, + CreatedAt: time.Now().UTC(), + UpdatedAt: time.Now().UTC(), + } + + // Call the repository to persist the transaction + createdTransaction, err := s.repo.Create(ctx, transaction) + if err != nil { + cacheErr := s.store.SetByUserId(ctx, userID, []entities.Transaction{*createdTransaction}) + if cacheErr != nil { + s.log.Warnf("Failed to cache transaction for userID %s: %v", userID, cacheErr) + } + return nil, fmt.Errorf("failed to create transaction: %w", err) + } + + // Return the created transaction + return createdTransaction, nil } func (s *Service) GetTransactions(ctx context.Context, userID string) ([]entities.Transaction, error) { - return nil, nil + objectID, err := primitive.ObjectIDFromHex(userID) + if err != nil { + return nil, fmt.Errorf("invalid user ID: %w", err) + } + // Check if transactions are cached + cachedTransactions, err := s.store.GetByUserId(ctx, userID) + if err != nil && err.Error() != "redis: nil" { + return nil, fmt.Errorf("failed to get cached transactions: %w", err) + } + if cachedTransactions != nil { + return cachedTransactions, nil + } + // If not cached, fetch from the repository + transactions, err := s.repo.FindByUserId(ctx, objectID) + if err != nil { + return nil, fmt.Errorf("failed to get transactions: %w", err) + } + + // Cache the fetched transactions + cacheErr := s.store.SetByUserId(ctx, userID, transactions) + if cacheErr != nil { + s.log.Warnf("Failed to cache transaction for userID %s: %v", userID, cacheErr) + } + return transactions, nil } From aed302db93123d1430e2f042980e2244682da761 Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Fri, 25 Apr 2025 21:19:27 +0800 Subject: [PATCH 2/5] Refactor transaction caching logic in CreateTransaction method to ensure caching occurs after transaction creation --- internal/module/transaction/usecase/service.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/module/transaction/usecase/service.go b/internal/module/transaction/usecase/service.go index 31761bf..6504bb8 100644 --- a/internal/module/transaction/usecase/service.go +++ b/internal/module/transaction/usecase/service.go @@ -52,14 +52,15 @@ func (s *Service) CreateTransaction(ctx context.Context, userID string, req *dto // Call the repository to persist the transaction createdTransaction, err := s.repo.Create(ctx, transaction) if err != nil { - cacheErr := s.store.SetByUserId(ctx, userID, []entities.Transaction{*createdTransaction}) - if cacheErr != nil { - s.log.Warnf("Failed to cache transaction for userID %s: %v", userID, cacheErr) - } return nil, fmt.Errorf("failed to create transaction: %w", err) } // Return the created transaction + cacheErr := s.store.SetByUserId(ctx, userID, []entities.Transaction{*createdTransaction}) + if cacheErr != nil { + s.log.Warnf("Failed to cache transaction for userID %s: %v", userID, cacheErr) + } + return createdTransaction, nil } From 1cffe63e2ed7e8131ff67134352773575ce825d8 Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Fri, 25 Apr 2025 21:35:39 +0800 Subject: [PATCH 3/5] Enhance transaction caching in CreateTransaction method to store newly created transactions in Redis --- .../infrastructure/persistence/redis/transaction.go | 8 +------- internal/module/transaction/usecase/service.go | 12 ++++++++++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/internal/infrastructure/persistence/redis/transaction.go b/internal/infrastructure/persistence/redis/transaction.go index 75c9ca0..871016a 100644 --- a/internal/infrastructure/persistence/redis/transaction.go +++ b/internal/infrastructure/persistence/redis/transaction.go @@ -2,7 +2,6 @@ package redis import ( "context" - "encoding/json" "fmt" "time" @@ -32,12 +31,7 @@ func (s *TransactionStore) GetByUserId(ctx context.Context, userID string) ([]en } func (s *TransactionStore) SetByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error { - data, err := json.Marshal(transactions) - if err != nil { - return err - } - - return s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), data, transactionCacheTTL) + return s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), transactions, transactionCacheTTL) } func (s *TransactionStore) DeleteByUserId(ctx context.Context, userID string) error { diff --git a/internal/module/transaction/usecase/service.go b/internal/module/transaction/usecase/service.go index 6504bb8..a124f56 100644 --- a/internal/module/transaction/usecase/service.go +++ b/internal/module/transaction/usecase/service.go @@ -55,8 +55,16 @@ func (s *Service) CreateTransaction(ctx context.Context, userID string, req *dto return nil, fmt.Errorf("failed to create transaction: %w", err) } - // Return the created transaction - cacheErr := s.store.SetByUserId(ctx, userID, []entities.Transaction{*createdTransaction}) + cachedTransactions, err := s.store.GetByUserId(ctx, userID) + if err != nil && err.Error() != "redis: nil" { + return nil, fmt.Errorf("failed to get cached transactions: %w", err) + } + if cachedTransactions != nil { + cachedTransactions = append(cachedTransactions, *createdTransaction) + } else { + cachedTransactions = []entities.Transaction{*createdTransaction} + } + cacheErr := s.store.SetByUserId(ctx, userID, cachedTransactions) if cacheErr != nil { s.log.Warnf("Failed to cache transaction for userID %s: %v", userID, cacheErr) } From 2c55793440070e2e88a9b20fc19677fcf65aa8d7 Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Fri, 25 Apr 2025 22:52:52 +0800 Subject: [PATCH 4/5] Implement AddByUserId and SetMultipleByUserId methods in TransactionStore; update service and tests for caching transactions --- .../persistence/mongodb/transaction.go | 1 + .../persistence/redis/transaction.go | 30 ++++- .../persistence/redis/transaction_test.go | 116 ++++++++++++++++-- .../transaction/repository/repository.go | 3 +- .../transaction/repository/repository_mock.go | 45 ++++--- .../module/transaction/usecase/service.go | 13 +- 6 files changed, 170 insertions(+), 38 deletions(-) diff --git a/internal/infrastructure/persistence/mongodb/transaction.go b/internal/infrastructure/persistence/mongodb/transaction.go index a4ce4a2..c22d70a 100644 --- a/internal/infrastructure/persistence/mongodb/transaction.go +++ b/internal/infrastructure/persistence/mongodb/transaction.go @@ -22,6 +22,7 @@ func NewTransactionRepository(db MongoClient) transaction_repository.Repository } func (r *MongoTransactionRepository) Create(ctx context.Context, entity *entities.Transaction) (*entities.Transaction, error) { + entity.ID = primitive.NewObjectID() _, err := r.collection.InsertOne(ctx, entity) if err != nil { return nil, err diff --git a/internal/infrastructure/persistence/redis/transaction.go b/internal/infrastructure/persistence/redis/transaction.go index 871016a..7e1b9cd 100644 --- a/internal/infrastructure/persistence/redis/transaction.go +++ b/internal/infrastructure/persistence/redis/transaction.go @@ -2,10 +2,12 @@ package redis import ( "context" + "errors" "fmt" "time" "github.com/Financial-Partner/server/internal/entities" + "github.com/redis/go-redis/v9" ) const ( @@ -30,8 +32,32 @@ func (s *TransactionStore) GetByUserId(ctx context.Context, userID string) ([]en return transactions, nil } -func (s *TransactionStore) SetByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error { - return s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), transactions, transactionCacheTTL) +func (s *TransactionStore) AddByUserId(ctx context.Context, userID string, transaction *entities.Transaction) error { + transactions, err := s.GetByUserId(ctx, userID) + if err != nil && !errors.Is(err, redis.Nil) { + return err + } + + transactions = append(transactions, *transaction) + err = s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), transactions, transactionCacheTTL) + if err != nil { + return err + } + return nil +} + +func (s *TransactionStore) SetMultipleByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error { + existingTransactions, err := s.GetByUserId(ctx, userID) + if err != nil && !errors.Is(err, redis.Nil) { + return err + } + + transactions = append(existingTransactions, transactions...) + err = s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), transactions, transactionCacheTTL) + if err != nil { + return err + } + return nil } func (s *TransactionStore) DeleteByUserId(ctx context.Context, userID string) error { diff --git a/internal/infrastructure/persistence/redis/transaction_test.go b/internal/infrastructure/persistence/redis/transaction_test.go index be55c79..7947a68 100644 --- a/internal/infrastructure/persistence/redis/transaction_test.go +++ b/internal/infrastructure/persistence/redis/transaction_test.go @@ -30,7 +30,7 @@ func TestTransactionStore(t *testing.T) { mockTransactions := []entities.Transaction{ { ID: primitive.NewObjectID(), - UserID: primitive.NewObjectID(), + UserID: userID, Amount: 100, Description: "Groceries", Date: time.Now(), @@ -41,7 +41,7 @@ func TestTransactionStore(t *testing.T) { }, { ID: primitive.NewObjectID(), - UserID: primitive.NewObjectID(), + UserID: userID, Amount: 200, Description: "Rent", Date: time.Now(), @@ -78,16 +78,16 @@ func TestTransactionStore(t *testing.T) { assert.Nil(t, transactions) }) - t.Run("SetByUserIdSuccess", func(t *testing.T) { + t.Run("AddByUserIdSuccess", func(t *testing.T) { mockRedisClient := redis.NewMockRedisClient(ctrl) transactionStore := redis.NewTransactionStore(mockRedisClient) - // mock data + // Mock data userID := primitive.NewObjectID() - mockTransactions := []entities.Transaction{ + existingTransactions := []entities.Transaction{ { ID: primitive.NewObjectID(), - UserID: primitive.NewObjectID(), + UserID: userID, Amount: 100, Description: "Groceries", Date: time.Now(), @@ -96,9 +96,70 @@ func TestTransactionStore(t *testing.T) { CreatedAt: time.Now(), UpdatedAt: time.Now(), }, + } + newTransaction := &entities.Transaction{ + ID: primitive.NewObjectID(), + UserID: userID, + Amount: 200, + Description: "Rent", + Date: time.Now(), + Category: "Housing", + Type: "expense", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + + mockData, _ := json.Marshal(existingTransactions) + + mockRedisClient.EXPECT().Get( + gomock.Any(), + fmt.Sprintf("user:%s:transactions", userID.Hex()), + gomock.Any(), + ).DoAndReturn( + func(_ context.Context, _ string, dest interface{}) error { + return json.Unmarshal(mockData, dest) + }, + ) + + mockRedisClient.EXPECT().Set( + gomock.Any(), + fmt.Sprintf("user:%s:transactions", userID.Hex()), + gomock.Any(), + gomock.Any(), + ).DoAndReturn( + func(_ context.Context, _ string, value interface{}, _ time.Duration) error { + return nil + }, + ) + + // Call AddByUserId + err := transactionStore.AddByUserId(context.Background(), userID.Hex(), newTransaction) + require.NoError(t, err) + }) + + t.Run("SetMultipleByUserIdWithExistingTransactions", func(t *testing.T) { + mockRedisClient := redis.NewMockRedisClient(ctrl) + transactionStore := redis.NewTransactionStore(mockRedisClient) + + // Mock data + userID := primitive.NewObjectID() + existingTransactions := []entities.Transaction{ { ID: primitive.NewObjectID(), - UserID: primitive.NewObjectID(), + UserID: userID, + Amount: 100, + Description: "Groceries", + Date: time.Now(), + Category: "Food", + Type: "expense", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, + } + newTransactions := []entities.Transaction{ + { + ID: primitive.NewObjectID(), + UserID: userID, Amount: 200, Description: "Rent", Date: time.Now(), @@ -107,10 +168,47 @@ func TestTransactionStore(t *testing.T) { CreatedAt: time.Now(), UpdatedAt: time.Now(), }, + { + ID: primitive.NewObjectID(), + UserID: userID, + Amount: 50, + Description: "Utilities", + Date: time.Now(), + Category: "Bills", + Type: "expense", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }, } - mockRedisClient.EXPECT().Set(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - err := transactionStore.SetByUserId(context.Background(), userID.Hex(), mockTransactions) + // Serialize existing transactions to JSON + mockData, _ := json.Marshal(existingTransactions) + + // Mock the Get method to return the existing transactions + mockRedisClient.EXPECT().Get( + gomock.Any(), + fmt.Sprintf("user:%s:transactions", userID.Hex()), + gomock.Any(), + ).DoAndReturn( + func(_ context.Context, _ string, dest interface{}) error { + return json.Unmarshal(mockData, dest) + }, + ) + + // Mock the Set method to save the updated transactions + mockRedisClient.EXPECT().Set( + gomock.Any(), + fmt.Sprintf("user:%s:transactions", userID.Hex()), + gomock.Any(), + gomock.Any(), + ).DoAndReturn( + func(_ context.Context, _ string, value interface{}, _ time.Duration) error { + return nil + }, + ) + + // Call SetMultipleByUserId + err := transactionStore.SetMultipleByUserId(context.Background(), userID.Hex(), newTransactions) require.NoError(t, err) }) diff --git a/internal/module/transaction/repository/repository.go b/internal/module/transaction/repository/repository.go index 49ed31f..fe75ba9 100644 --- a/internal/module/transaction/repository/repository.go +++ b/internal/module/transaction/repository/repository.go @@ -17,6 +17,7 @@ type Repository interface { type TransactionStore interface { GetByUserId(ctx context.Context, userID string) ([]entities.Transaction, error) - SetByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error DeleteByUserId(ctx context.Context, userID string) error + AddByUserId(ctx context.Context, userID string, transaction *entities.Transaction) error + SetMultipleByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error } diff --git a/internal/module/transaction/repository/repository_mock.go b/internal/module/transaction/repository/repository_mock.go index 20c2ca3..31780f9 100644 --- a/internal/module/transaction/repository/repository_mock.go +++ b/internal/module/transaction/repository/repository_mock.go @@ -14,6 +14,7 @@ import ( reflect "reflect" entities "github.com/Financial-Partner/server/internal/entities" + primitive "go.mongodb.org/mongo-driver/bson/primitive" gomock "go.uber.org/mock/gomock" ) @@ -57,7 +58,7 @@ func (mr *MockRepositoryMockRecorder) Create(ctx, transaction any) *gomock.Call } // FindByUserId mocks base method. -func (m *MockRepository) FindByUserId(ctx context.Context, userID string) ([]entities.Transaction, error) { +func (m *MockRepository) FindByUserId(ctx context.Context, userID primitive.ObjectID) ([]entities.Transaction, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FindByUserId", ctx, userID) ret0, _ := ret[0].([]entities.Transaction) @@ -95,6 +96,34 @@ func (m *MockTransactionStore) EXPECT() *MockTransactionStoreMockRecorder { return m.recorder } +// AddByUserId mocks base method. +func (m *MockTransactionStore) AddByUserId(ctx context.Context, userID string, transaction *entities.Transaction) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AddByUserId", ctx, userID, transaction) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddByUserId indicates an expected call of AddByUserId. +func (mr *MockTransactionStoreMockRecorder) AddByUserId(ctx, userID, transaction any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddByUserId", reflect.TypeOf((*MockTransactionStore)(nil).AddByUserId), ctx, userID, transaction) +} + +// SetMultipleByUserId mocks base method. +func (m *MockTransactionStore) SetMultipleByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetMultipleByUserId", ctx, userID, transactions) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetMultipleByUserId indicates an expected call of SetMultipleByUserId. +func (mr *MockTransactionStoreMockRecorder) SetMultipleByUserId(ctx, userID, transactions any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMultipleByUserId", reflect.TypeOf((*MockTransactionStore)(nil).SetMultipleByUserId), ctx, userID, transactions) +} + // DeleteByUserId mocks base method. func (m *MockTransactionStore) DeleteByUserId(ctx context.Context, userID string) error { m.ctrl.T.Helper() @@ -123,17 +152,3 @@ func (mr *MockTransactionStoreMockRecorder) GetByUserId(ctx, userID any) *gomock mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByUserId", reflect.TypeOf((*MockTransactionStore)(nil).GetByUserId), ctx, userID) } - -// SetByUserId mocks base method. -func (m *MockTransactionStore) SetByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SetByUserId", ctx, userID, transactions) - ret0, _ := ret[0].(error) - return ret0 -} - -// SetByUserId indicates an expected call of SetByUserId. -func (mr *MockTransactionStoreMockRecorder) SetByUserId(ctx, userID, transactions any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetByUserId", reflect.TypeOf((*MockTransactionStore)(nil).SetByUserId), ctx, userID, transactions) -} diff --git a/internal/module/transaction/usecase/service.go b/internal/module/transaction/usecase/service.go index a124f56..94d528f 100644 --- a/internal/module/transaction/usecase/service.go +++ b/internal/module/transaction/usecase/service.go @@ -55,16 +55,7 @@ func (s *Service) CreateTransaction(ctx context.Context, userID string, req *dto return nil, fmt.Errorf("failed to create transaction: %w", err) } - cachedTransactions, err := s.store.GetByUserId(ctx, userID) - if err != nil && err.Error() != "redis: nil" { - return nil, fmt.Errorf("failed to get cached transactions: %w", err) - } - if cachedTransactions != nil { - cachedTransactions = append(cachedTransactions, *createdTransaction) - } else { - cachedTransactions = []entities.Transaction{*createdTransaction} - } - cacheErr := s.store.SetByUserId(ctx, userID, cachedTransactions) + cacheErr := s.store.AddByUserId(ctx, userID, createdTransaction) if cacheErr != nil { s.log.Warnf("Failed to cache transaction for userID %s: %v", userID, cacheErr) } @@ -92,7 +83,7 @@ func (s *Service) GetTransactions(ctx context.Context, userID string) ([]entitie } // Cache the fetched transactions - cacheErr := s.store.SetByUserId(ctx, userID, transactions) + cacheErr := s.store.SetMultipleByUserId(ctx, userID, transactions) if cacheErr != nil { s.log.Warnf("Failed to cache transaction for userID %s: %v", userID, cacheErr) } From fb0310e9d4ca42162e9c124285500aced7b46e40 Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Fri, 25 Apr 2025 23:00:37 +0800 Subject: [PATCH 5/5] Remove AddByUserId method from TransactionStore and update service to delete transaction cache for userID --- .../persistence/redis/transaction.go | 14 ----- .../persistence/redis/transaction_test.go | 59 ------------------- .../transaction/repository/repository.go | 1 - .../module/transaction/usecase/service.go | 4 +- 4 files changed, 2 insertions(+), 76 deletions(-) diff --git a/internal/infrastructure/persistence/redis/transaction.go b/internal/infrastructure/persistence/redis/transaction.go index 7e1b9cd..3f67ee8 100644 --- a/internal/infrastructure/persistence/redis/transaction.go +++ b/internal/infrastructure/persistence/redis/transaction.go @@ -32,20 +32,6 @@ func (s *TransactionStore) GetByUserId(ctx context.Context, userID string) ([]en return transactions, nil } -func (s *TransactionStore) AddByUserId(ctx context.Context, userID string, transaction *entities.Transaction) error { - transactions, err := s.GetByUserId(ctx, userID) - if err != nil && !errors.Is(err, redis.Nil) { - return err - } - - transactions = append(transactions, *transaction) - err = s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), transactions, transactionCacheTTL) - if err != nil { - return err - } - return nil -} - func (s *TransactionStore) SetMultipleByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error { existingTransactions, err := s.GetByUserId(ctx, userID) if err != nil && !errors.Is(err, redis.Nil) { diff --git a/internal/infrastructure/persistence/redis/transaction_test.go b/internal/infrastructure/persistence/redis/transaction_test.go index 7947a68..27032bc 100644 --- a/internal/infrastructure/persistence/redis/transaction_test.go +++ b/internal/infrastructure/persistence/redis/transaction_test.go @@ -78,65 +78,6 @@ func TestTransactionStore(t *testing.T) { assert.Nil(t, transactions) }) - t.Run("AddByUserIdSuccess", func(t *testing.T) { - mockRedisClient := redis.NewMockRedisClient(ctrl) - transactionStore := redis.NewTransactionStore(mockRedisClient) - - // Mock data - userID := primitive.NewObjectID() - existingTransactions := []entities.Transaction{ - { - ID: primitive.NewObjectID(), - UserID: userID, - Amount: 100, - Description: "Groceries", - Date: time.Now(), - Category: "Food", - Type: "expense", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }, - } - newTransaction := &entities.Transaction{ - ID: primitive.NewObjectID(), - UserID: userID, - Amount: 200, - Description: "Rent", - Date: time.Now(), - Category: "Housing", - Type: "expense", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - - mockData, _ := json.Marshal(existingTransactions) - - mockRedisClient.EXPECT().Get( - gomock.Any(), - fmt.Sprintf("user:%s:transactions", userID.Hex()), - gomock.Any(), - ).DoAndReturn( - func(_ context.Context, _ string, dest interface{}) error { - return json.Unmarshal(mockData, dest) - }, - ) - - mockRedisClient.EXPECT().Set( - gomock.Any(), - fmt.Sprintf("user:%s:transactions", userID.Hex()), - gomock.Any(), - gomock.Any(), - ).DoAndReturn( - func(_ context.Context, _ string, value interface{}, _ time.Duration) error { - return nil - }, - ) - - // Call AddByUserId - err := transactionStore.AddByUserId(context.Background(), userID.Hex(), newTransaction) - require.NoError(t, err) - }) - t.Run("SetMultipleByUserIdWithExistingTransactions", func(t *testing.T) { mockRedisClient := redis.NewMockRedisClient(ctrl) transactionStore := redis.NewTransactionStore(mockRedisClient) diff --git a/internal/module/transaction/repository/repository.go b/internal/module/transaction/repository/repository.go index fe75ba9..68a87d4 100644 --- a/internal/module/transaction/repository/repository.go +++ b/internal/module/transaction/repository/repository.go @@ -18,6 +18,5 @@ type Repository interface { type TransactionStore interface { GetByUserId(ctx context.Context, userID string) ([]entities.Transaction, error) DeleteByUserId(ctx context.Context, userID string) error - AddByUserId(ctx context.Context, userID string, transaction *entities.Transaction) error SetMultipleByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error } diff --git a/internal/module/transaction/usecase/service.go b/internal/module/transaction/usecase/service.go index 94d528f..db1a1bc 100644 --- a/internal/module/transaction/usecase/service.go +++ b/internal/module/transaction/usecase/service.go @@ -55,9 +55,9 @@ func (s *Service) CreateTransaction(ctx context.Context, userID string, req *dto return nil, fmt.Errorf("failed to create transaction: %w", err) } - cacheErr := s.store.AddByUserId(ctx, userID, createdTransaction) + cacheErr := s.store.DeleteByUserId(ctx, userID) if cacheErr != nil { - s.log.Warnf("Failed to cache transaction for userID %s: %v", userID, cacheErr) + s.log.Warnf("Failed to delete transaction cache for userID %s: %v", userID, cacheErr) } return createdTransaction, nil