Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions cmd/server/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
}
Expand All @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions cmd/server/wire.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func InitializeServer(cfgFile string) (*Server, error) {
ProvideAuthService,
ProvideGoalService,
ProvideInvestmentService,
ProvideTransactionRepository,
ProvideTransactionStore,
ProvideTransactionService,
ProvideGachaService,
ProvideReportService,
Expand Down
4 changes: 3 additions & 1 deletion cmd/server/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 8 additions & 1 deletion internal/infrastructure/auth/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package auth
import (
"errors"

"go.mongodb.org/mongo-driver/bson/primitive"

"github.com/Financial-Partner/server/internal/config"
)

Expand All @@ -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
}
4 changes: 3 additions & 1 deletion internal/infrastructure/persistence/mongodb/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -21,14 +22,15 @@ 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
}
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
16 changes: 11 additions & 5 deletions internal/infrastructure/persistence/redis/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ package redis

import (
"context"
"encoding/json"
"errors"
"fmt"
"time"

"github.com/Financial-Partner/server/internal/entities"
"github.com/redis/go-redis/v9"
)

const (
Expand All @@ -31,13 +32,18 @@ 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 {
data, err := json.Marshal(transactions)
if err != 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
}

return s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), data, transactionCacheTTL)
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 {
Expand Down
75 changes: 57 additions & 18 deletions internal/infrastructure/persistence/redis/transaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,12 @@ func TestTransactionStore(t *testing.T) {
transactionStore := redis.NewTransactionStore(mockRedisClient)

// Mock data
userID := primitive.NewObjectID().Hex()
userID := primitive.NewObjectID()

mockTransactions := []entities.Transaction{
{
ID: primitive.NewObjectID(),
UserID: primitive.NewObjectID(),
UserID: userID,
Amount: 100,
Description: "Groceries",
Date: time.Now(),
Expand All @@ -41,7 +41,7 @@ func TestTransactionStore(t *testing.T) {
},
{
ID: primitive.NewObjectID(),
UserID: primitive.NewObjectID(),
UserID: userID,
Amount: 200,
Description: "Rent",
Date: time.Now(),
Expand All @@ -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)
})
Expand All @@ -70,24 +70,24 @@ 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)
})

t.Run("SetByUserIdSuccess", func(t *testing.T) {
t.Run("SetMultipleByUserIdWithExistingTransactions", func(t *testing.T) {
mockRedisClient := redis.NewMockRedisClient(ctrl)
transactionStore := redis.NewTransactionStore(mockRedisClient)

// mock data
userID := primitive.NewObjectID().Hex()
mockTransactions := []entities.Transaction{
// Mock data
userID := primitive.NewObjectID()
existingTransactions := []entities.Transaction{
{
ID: primitive.NewObjectID(),
UserID: primitive.NewObjectID(),
UserID: userID,
Amount: 100,
Description: "Groceries",
Date: time.Now(),
Expand All @@ -96,9 +96,11 @@ func TestTransactionStore(t *testing.T) {
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}
newTransactions := []entities.Transaction{
{
ID: primitive.NewObjectID(),
UserID: primitive.NewObjectID(),
UserID: userID,
Amount: 200,
Description: "Rent",
Date: time.Now(),
Expand All @@ -107,21 +109,58 @@ 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, 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)
})

t.Run("DeleteTransactionSuccess", func(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)
})
}
18 changes: 9 additions & 9 deletions internal/interfaces/http/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
}

Expand All @@ -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
Expand All @@ -102,20 +102,20 @@ 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
}

resp := dto.ReportSummaryResponse{
Summary: reportSummary.Summary,
}

responde.WithJSON(w, r, resp, http.StatusOK)
respond.WithJSON(w, r, resp, http.StatusOK)
}
2 changes: 1 addition & 1 deletion internal/interfaces/http/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading
Loading