Skip to content

Commit 4cc7cf7

Browse files
authored
Merge pull request #14 from Financial-Partner/feat/implement_transaction_service
Implement Repository and TransactionStore Interfaces for Database and Cache Operations
2 parents df302da + 6b872aa commit 4cc7cf7

9 files changed

Lines changed: 590 additions & 1 deletion

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package mongodb
2+
3+
import (
4+
"context"
5+
6+
"go.mongodb.org/mongo-driver/bson"
7+
"go.mongodb.org/mongo-driver/mongo"
8+
9+
"github.com/Financial-Partner/server/internal/entities"
10+
transaction_repository "github.com/Financial-Partner/server/internal/module/transaction/repository"
11+
)
12+
13+
type MongoTransactionRepository struct {
14+
collection *mongo.Collection
15+
}
16+
17+
func NewTransactionRepository(db MongoClient) transaction_repository.Repository {
18+
return &MongoTransactionRepository{
19+
collection: db.Collection("transactions"),
20+
}
21+
}
22+
23+
func (r *MongoTransactionRepository) Create(ctx context.Context, entity *entities.Transaction) (*entities.Transaction, error) {
24+
_, err := r.collection.InsertOne(ctx, entity)
25+
if err != nil {
26+
return nil, err
27+
}
28+
return entity, nil
29+
}
30+
31+
func (r *MongoTransactionRepository) FindByUserId(ctx context.Context, userID string) ([]entities.Transaction, error) {
32+
var transactions []entities.Transaction
33+
cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID})
34+
if err != nil {
35+
return nil, err
36+
}
37+
defer cursor.Close(ctx)
38+
39+
if err := cursor.All(ctx, &transactions); err != nil {
40+
return nil, err
41+
}
42+
43+
return transactions, nil
44+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package mongodb_test
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/Financial-Partner/server/internal/entities"
9+
"github.com/Financial-Partner/server/internal/infrastructure/persistence/mongodb"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
"go.mongodb.org/mongo-driver/bson"
13+
"go.mongodb.org/mongo-driver/bson/primitive"
14+
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
15+
)
16+
17+
func TestMongoTransactionRepository(t *testing.T) {
18+
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
19+
20+
testUserID := primitive.NewObjectID().Hex()
21+
testTransactions := []entities.Transaction{
22+
{
23+
ID: primitive.NewObjectID(),
24+
UserID: testUserID,
25+
Amount: 100,
26+
Description: "Dinner",
27+
Date: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC),
28+
Category: "Food",
29+
Type: "expense",
30+
CreatedAt: time.Now(),
31+
UpdatedAt: time.Now(),
32+
},
33+
{
34+
ID: primitive.NewObjectID(),
35+
UserID: testUserID,
36+
Amount: 200,
37+
Description: "Rent",
38+
Date: time.Date(2023, time.January, 2, 0, 0, 0, 0, time.UTC),
39+
Category: "Housing",
40+
Type: "expense",
41+
CreatedAt: time.Now(),
42+
UpdatedAt: time.Now(),
43+
},
44+
}
45+
46+
// Convert transactions to BSON documents
47+
var testTransactionDocs []bson.D
48+
for _, transaction := range testTransactions {
49+
transactionBSON, err := bson.Marshal(transaction)
50+
require.NoError(t, err)
51+
var transactionDoc bson.D
52+
err = bson.Unmarshal(transactionBSON, &transactionDoc)
53+
require.NoError(t, err)
54+
testTransactionDocs = append(testTransactionDocs, transactionDoc)
55+
}
56+
57+
t.Run("FindByUserId", func(t *testing.T) {
58+
mt.Run("success", func(mt *mtest.T) {
59+
mt.AddMockResponses(
60+
mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, testTransactionDocs...),
61+
mtest.CreateCursorResponse(0, "foo.bar", mtest.NextBatch), // Simulate end of cursor
62+
)
63+
repo := mongodb.NewTransactionRepository(mt.Client.Database("testdb"))
64+
result, err := repo.FindByUserId(context.Background(), testUserID)
65+
assert.NoError(t, err)
66+
assert.NotNil(t, result)
67+
assert.Len(t, result, len(testTransactions))
68+
69+
// Validate each transaction
70+
for i, transaction := range result {
71+
assert.Equal(t, testTransactions[i].ID, transaction.ID)
72+
assert.Equal(t, testTransactions[i].UserID, transaction.UserID)
73+
assert.Equal(t, testTransactions[i].Amount, transaction.Amount)
74+
assert.Equal(t, testTransactions[i].Description, transaction.Description)
75+
_, err := time.Parse(time.DateOnly, transaction.Date.Format(time.DateOnly))
76+
require.NoError(t, err)
77+
assert.Equal(t, testTransactions[i].Category, transaction.Category)
78+
assert.Equal(t, testTransactions[i].Type, transaction.Type)
79+
}
80+
})
81+
mt.Run("not found", func(mt *mtest.T) {
82+
mt.AddMockResponses(mtest.CreateCursorResponse(0, "foo.bar", mtest.FirstBatch))
83+
repo := mongodb.NewTransactionRepository(mt.DB)
84+
result, err := repo.FindByUserId(context.Background(), testUserID)
85+
assert.Nil(t, err)
86+
assert.Nil(t, result)
87+
})
88+
mt.Run("database error", func(mt *mtest.T) {
89+
mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{
90+
Code: 11000,
91+
Message: "database error",
92+
}))
93+
repo := mongodb.NewTransactionRepository(mt.DB)
94+
result, err := repo.FindByUserId(context.Background(), testUserID)
95+
assert.Error(t, err)
96+
assert.Nil(t, result)
97+
})
98+
})
99+
100+
t.Run("Create", func(t *testing.T) {
101+
mt.Run("success", func(mt *mtest.T) {
102+
mt.AddMockResponses(mtest.CreateSuccessResponse())
103+
repo := mongodb.NewTransactionRepository(mt.DB)
104+
result, err := repo.Create(context.Background(), &(testTransactions[0]))
105+
assert.NoError(t, err)
106+
assert.NotNil(t, result)
107+
assert.Equal(t, testTransactions[0].ID, result.ID)
108+
assert.Equal(t, testTransactions[0].UserID, result.UserID)
109+
assert.Equal(t, testTransactions[0].Amount, result.Amount)
110+
assert.Equal(t, testTransactions[0].Description, result.Description)
111+
assert.Equal(t, testTransactions[0].Date, result.Date)
112+
assert.Equal(t, testTransactions[0].Category, result.Category)
113+
assert.Equal(t, testTransactions[0].Type, result.Type)
114+
})
115+
mt.Run("error", func(mt *mtest.T) {
116+
mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{
117+
Code: 11000,
118+
Message: "duplicate key error",
119+
}))
120+
repo := mongodb.NewTransactionRepository(mt.DB)
121+
result, err := repo.Create(context.Background(), &(testTransactions[0]))
122+
assert.Error(t, err)
123+
assert.Nil(t, result)
124+
})
125+
})
126+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package redis
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"time"
8+
9+
"github.com/Financial-Partner/server/internal/entities"
10+
)
11+
12+
const (
13+
transactionCacheKey = "user:%s:transactions"
14+
transactionCacheTTL = time.Hour * 24
15+
)
16+
17+
type TransactionStore struct {
18+
cacheClient RedisClient
19+
}
20+
21+
func NewTransactionStore(cacheClient RedisClient) *TransactionStore {
22+
return &TransactionStore{cacheClient: cacheClient}
23+
}
24+
25+
func (s *TransactionStore) GetByUserId(ctx context.Context, userID string) ([]entities.Transaction, error) {
26+
var transactions []entities.Transaction
27+
err := s.cacheClient.Get(ctx, fmt.Sprintf(transactionCacheKey, userID), &transactions)
28+
if err != nil {
29+
return nil, err
30+
}
31+
return transactions, nil
32+
}
33+
34+
func (s *TransactionStore) SetByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error {
35+
data, err := json.Marshal(transactions)
36+
if err != nil {
37+
return err
38+
}
39+
40+
return s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), data, transactionCacheTTL)
41+
}
42+
43+
func (s *TransactionStore) DeleteByUserId(ctx context.Context, userID string) error {
44+
return s.cacheClient.Delete(ctx, fmt.Sprintf(transactionCacheKey, userID))
45+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package redis_test
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"testing"
8+
"time"
9+
10+
"github.com/Financial-Partner/server/internal/entities"
11+
"github.com/Financial-Partner/server/internal/infrastructure/persistence/redis"
12+
goredis "github.com/redis/go-redis/v9"
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
"go.mongodb.org/mongo-driver/bson/primitive"
16+
"go.uber.org/mock/gomock"
17+
)
18+
19+
func TestTransactionStore(t *testing.T) {
20+
ctrl := gomock.NewController(t)
21+
defer ctrl.Finish()
22+
23+
t.Run("GetByUserIdSuccess", func(t *testing.T) {
24+
mockRedisClient := redis.NewMockRedisClient(ctrl)
25+
transactionStore := redis.NewTransactionStore(mockRedisClient)
26+
27+
// Mock data
28+
userID := primitive.NewObjectID().Hex()
29+
30+
mockTransactions := []entities.Transaction{
31+
{
32+
ID: primitive.NewObjectID(),
33+
UserID: userID,
34+
Amount: 100,
35+
Description: "Groceries",
36+
Date: time.Now(),
37+
Category: "Food",
38+
Type: "expense",
39+
CreatedAt: time.Now(),
40+
UpdatedAt: time.Now(),
41+
},
42+
{
43+
ID: primitive.NewObjectID(),
44+
UserID: userID,
45+
Amount: 200,
46+
Description: "Rent",
47+
Date: time.Now(),
48+
Category: "Housing",
49+
Type: "expense",
50+
CreatedAt: time.Now(),
51+
UpdatedAt: time.Now(),
52+
},
53+
}
54+
55+
// Serialize mockTransactions to JSON
56+
mockData, _ := json.Marshal(mockTransactions)
57+
58+
// Mock the Get method to return the serialized JSON data
59+
mockRedisClient.EXPECT().Get(gomock.Any(), fmt.Sprintf("user:%s:transactions", userID), gomock.Any()).DoAndReturn(
60+
func(_ context.Context, _ string, dest interface{}) error {
61+
return json.Unmarshal(mockData, dest)
62+
},
63+
)
64+
transactions, err := transactionStore.GetByUserId(context.Background(), userID)
65+
require.NoError(t, err)
66+
assert.NotNil(t, transactions)
67+
})
68+
69+
t.Run("GetByUserIdNotFound", func(t *testing.T) {
70+
mockRedisClient := redis.NewMockRedisClient(ctrl)
71+
transactionStore := redis.NewTransactionStore(mockRedisClient)
72+
73+
userID := primitive.NewObjectID().Hex()
74+
mockRedisClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(goredis.Nil)
75+
76+
transactions, err := transactionStore.GetByUserId(context.Background(), userID)
77+
require.Error(t, err)
78+
assert.Nil(t, transactions)
79+
})
80+
81+
t.Run("SetByUserIdSuccess", func(t *testing.T) {
82+
mockRedisClient := redis.NewMockRedisClient(ctrl)
83+
transactionStore := redis.NewTransactionStore(mockRedisClient)
84+
85+
// mock data
86+
userID := primitive.NewObjectID().Hex()
87+
mockTransactions := []entities.Transaction{
88+
{
89+
ID: primitive.NewObjectID(),
90+
UserID: userID,
91+
Amount: 100,
92+
Description: "Groceries",
93+
Date: time.Now(),
94+
Category: "Food",
95+
Type: "expense",
96+
CreatedAt: time.Now(),
97+
UpdatedAt: time.Now(),
98+
},
99+
{
100+
ID: primitive.NewObjectID(),
101+
UserID: userID,
102+
Amount: 200,
103+
Description: "Rent",
104+
Date: time.Now(),
105+
Category: "Housing",
106+
Type: "expense",
107+
CreatedAt: time.Now(),
108+
UpdatedAt: time.Now(),
109+
},
110+
}
111+
mockRedisClient.EXPECT().Set(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
112+
113+
err := transactionStore.SetByUserId(context.Background(), userID, mockTransactions)
114+
require.NoError(t, err)
115+
})
116+
117+
t.Run("DeleteTransactionSuccess", func(t *testing.T) {
118+
mockRedisClient := redis.NewMockRedisClient(ctrl)
119+
transactionStore := redis.NewTransactionStore(mockRedisClient)
120+
121+
userID := primitive.NewObjectID().Hex()
122+
mockRedisClient.EXPECT().Delete(gomock.Any(), fmt.Sprintf("user:%s:transactions", userID)).Return(nil)
123+
124+
err := transactionStore.DeleteByUserId(context.Background(), userID)
125+
require.NoError(t, err)
126+
})
127+
}

internal/interfaces/http/transaction.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
type TransactionService interface {
1919
CreateTransaction(ctx context.Context, UserID string, transaction *dto.CreateTransactionRequest) (*entities.Transaction, error)
20-
GetTransactions(ctx context.Context, UserId string) ([]entities.Transaction, error)
20+
GetTransactions(ctx context.Context, UserID string) ([]entities.Transaction, error)
2121
}
2222

2323
// @Summary Get transactions
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package transaction_domain
2+
3+
import (
4+
"context"
5+
6+
"github.com/Financial-Partner/server/internal/entities"
7+
"github.com/Financial-Partner/server/internal/interfaces/http/dto"
8+
)
9+
10+
//go:generate mockgen -source=interfaces.go -destination=interfaces_mock.go -package=transaction_domain
11+
12+
type TransactionService interface {
13+
CreateTransaction(ctx context.Context, UserID string, transaction *dto.CreateTransactionRequest) (*entities.Transaction, error)
14+
GetTransactions(ctx context.Context, UserId string) ([]entities.Transaction, error)
15+
}

0 commit comments

Comments
 (0)