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
44 changes: 44 additions & 0 deletions internal/infrastructure/persistence/mongodb/transaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package mongodb

import (
"context"

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

"github.com/Financial-Partner/server/internal/entities"
transaction_repository "github.com/Financial-Partner/server/internal/module/transaction/repository"
)

type MongoTransactionRepository struct {
collection *mongo.Collection
}

func NewTransactionRepository(db MongoClient) transaction_repository.Repository {
return &MongoTransactionRepository{
collection: db.Collection("transactions"),
}
}

func (r *MongoTransactionRepository) Create(ctx context.Context, entity *entities.Transaction) (*entities.Transaction, error) {
_, 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) {
var transactions []entities.Transaction
cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID})
if err != nil {
return nil, err
}
defer cursor.Close(ctx)

if err := cursor.All(ctx, &transactions); err != nil {
return nil, err
}

return transactions, nil
}
126 changes: 126 additions & 0 deletions internal/infrastructure/persistence/mongodb/transaction_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package mongodb_test

import (
"context"
"testing"
"time"

"github.com/Financial-Partner/server/internal/entities"
"github.com/Financial-Partner/server/internal/infrastructure/persistence/mongodb"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)

func TestMongoTransactionRepository(t *testing.T) {
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))

testUserID := primitive.NewObjectID().Hex()
testTransactions := []entities.Transaction{
{
ID: primitive.NewObjectID(),
UserID: testUserID,
Amount: 100,
Description: "Dinner",
Date: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC),
Category: "Food",
Type: "expense",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
{
ID: primitive.NewObjectID(),
UserID: testUserID,
Amount: 200,
Description: "Rent",
Date: time.Date(2023, time.January, 2, 0, 0, 0, 0, time.UTC),
Category: "Housing",
Type: "expense",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}

// Convert transactions to BSON documents
var testTransactionDocs []bson.D
for _, transaction := range testTransactions {
transactionBSON, err := bson.Marshal(transaction)
require.NoError(t, err)
var transactionDoc bson.D
err = bson.Unmarshal(transactionBSON, &transactionDoc)
require.NoError(t, err)
testTransactionDocs = append(testTransactionDocs, transactionDoc)
}

t.Run("FindByUserId", func(t *testing.T) {
mt.Run("success", func(mt *mtest.T) {
mt.AddMockResponses(
mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, testTransactionDocs...),
mtest.CreateCursorResponse(0, "foo.bar", mtest.NextBatch), // Simulate end of cursor
)
repo := mongodb.NewTransactionRepository(mt.Client.Database("testdb"))
result, err := repo.FindByUserId(context.Background(), testUserID)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Len(t, result, len(testTransactions))

// Validate each transaction
for i, transaction := range result {
assert.Equal(t, testTransactions[i].ID, transaction.ID)
assert.Equal(t, testTransactions[i].UserID, transaction.UserID)
assert.Equal(t, testTransactions[i].Amount, transaction.Amount)
assert.Equal(t, testTransactions[i].Description, transaction.Description)
_, err := time.Parse(time.DateOnly, transaction.Date.Format(time.DateOnly))
require.NoError(t, err)
assert.Equal(t, testTransactions[i].Category, transaction.Category)
assert.Equal(t, testTransactions[i].Type, transaction.Type)
}
})
mt.Run("not found", func(mt *mtest.T) {
mt.AddMockResponses(mtest.CreateCursorResponse(0, "foo.bar", mtest.FirstBatch))
repo := mongodb.NewTransactionRepository(mt.DB)
result, err := repo.FindByUserId(context.Background(), testUserID)
assert.Nil(t, err)
assert.Nil(t, result)
})
mt.Run("database error", func(mt *mtest.T) {
mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{
Code: 11000,
Message: "database error",
}))
repo := mongodb.NewTransactionRepository(mt.DB)
result, err := repo.FindByUserId(context.Background(), testUserID)
assert.Error(t, err)
assert.Nil(t, result)
})
})

t.Run("Create", func(t *testing.T) {
mt.Run("success", func(mt *mtest.T) {
mt.AddMockResponses(mtest.CreateSuccessResponse())
repo := mongodb.NewTransactionRepository(mt.DB)
result, err := repo.Create(context.Background(), &(testTransactions[0]))
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, testTransactions[0].ID, result.ID)
assert.Equal(t, testTransactions[0].UserID, result.UserID)
assert.Equal(t, testTransactions[0].Amount, result.Amount)
assert.Equal(t, testTransactions[0].Description, result.Description)
assert.Equal(t, testTransactions[0].Date, result.Date)
assert.Equal(t, testTransactions[0].Category, result.Category)
assert.Equal(t, testTransactions[0].Type, result.Type)
})
mt.Run("error", func(mt *mtest.T) {
mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{
Code: 11000,
Message: "duplicate key error",
}))
repo := mongodb.NewTransactionRepository(mt.DB)
result, err := repo.Create(context.Background(), &(testTransactions[0]))
assert.Error(t, err)
assert.Nil(t, result)
})
})
}
45 changes: 45 additions & 0 deletions internal/infrastructure/persistence/redis/transaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package redis

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

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

const (
transactionCacheKey = "user:%s:transactions"
transactionCacheTTL = time.Hour * 24
)

type TransactionStore struct {
cacheClient RedisClient
}

func NewTransactionStore(cacheClient RedisClient) *TransactionStore {
return &TransactionStore{cacheClient: cacheClient}
}

func (s *TransactionStore) GetByUserId(ctx context.Context, userID string) ([]entities.Transaction, error) {
var transactions []entities.Transaction
err := s.cacheClient.Get(ctx, fmt.Sprintf(transactionCacheKey, userID), &transactions)
if err != nil {
return nil, err
}
return transactions, nil
}

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)
}

func (s *TransactionStore) DeleteByUserId(ctx context.Context, userID string) error {
return s.cacheClient.Delete(ctx, fmt.Sprintf(transactionCacheKey, userID))
}
127 changes: 127 additions & 0 deletions internal/infrastructure/persistence/redis/transaction_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package redis_test

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

"github.com/Financial-Partner/server/internal/entities"
"github.com/Financial-Partner/server/internal/infrastructure/persistence/redis"
goredis "github.com/redis/go-redis/v9"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/mock/gomock"
)

func TestTransactionStore(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()

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

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

mockTransactions := []entities.Transaction{
{
ID: primitive.NewObjectID(),
UserID: userID,
Amount: 100,
Description: "Groceries",
Date: time.Now(),
Category: "Food",
Type: "expense",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
{
ID: primitive.NewObjectID(),
UserID: userID,
Amount: 200,
Description: "Rent",
Date: time.Now(),
Category: "Housing",
Type: "expense",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
}

// Serialize mockTransactions to JSON
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(
func(_ context.Context, _ string, dest interface{}) error {
return json.Unmarshal(mockData, dest)
},
)
transactions, err := transactionStore.GetByUserId(context.Background(), userID)
require.NoError(t, err)
assert.NotNil(t, transactions)
})

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

userID := primitive.NewObjectID().Hex()
mockRedisClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(goredis.Nil)

transactions, err := transactionStore.GetByUserId(context.Background(), userID)
require.Error(t, err)
assert.Nil(t, transactions)
})

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

// mock data
userID := primitive.NewObjectID().Hex()
mockTransactions := []entities.Transaction{
{
ID: primitive.NewObjectID(),
UserID: userID,
Amount: 100,
Description: "Groceries",
Date: time.Now(),
Category: "Food",
Type: "expense",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
},
{
ID: primitive.NewObjectID(),
UserID: userID,
Amount: 200,
Description: "Rent",
Date: time.Now(),
Category: "Housing",
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)
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)

err := transactionStore.DeleteByUserId(context.Background(), userID)
require.NoError(t, err)
})
}
2 changes: 1 addition & 1 deletion internal/interfaces/http/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (

type TransactionService interface {
CreateTransaction(ctx context.Context, UserID string, transaction *dto.CreateTransactionRequest) (*entities.Transaction, error)
GetTransactions(ctx context.Context, UserId string) ([]entities.Transaction, error)
GetTransactions(ctx context.Context, UserID string) ([]entities.Transaction, error)
}

// @Summary Get transactions
Expand Down
15 changes: 15 additions & 0 deletions internal/module/transaction/domain/interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package transaction_domain

import (
"context"

"github.com/Financial-Partner/server/internal/entities"
"github.com/Financial-Partner/server/internal/interfaces/http/dto"
)

//go:generate mockgen -source=interfaces.go -destination=interfaces_mock.go -package=transaction_domain

type TransactionService interface {
CreateTransaction(ctx context.Context, UserID string, transaction *dto.CreateTransactionRequest) (*entities.Transaction, error)
GetTransactions(ctx context.Context, UserId string) ([]entities.Transaction, error)
}
Loading
Loading