Skip to content

Commit 0c7d92c

Browse files
authored
Merge pull request #17 from Financial-Partner/feat/implement_transaction_service
Implement transaction service
2 parents 0d31314 + fb0310e commit 0c7d92c

14 files changed

Lines changed: 221 additions & 69 deletions

File tree

cmd/server/providers.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
goal_usecase "github.com/Financial-Partner/server/internal/module/goal/usecase"
2121
investment_usecase "github.com/Financial-Partner/server/internal/module/investment/usecase"
2222
report_usecase "github.com/Financial-Partner/server/internal/module/report/usecase"
23+
transaction_repository "github.com/Financial-Partner/server/internal/module/transaction/repository"
2324
transaction_usecase "github.com/Financial-Partner/server/internal/module/transaction/usecase"
2425
user_repository "github.com/Financial-Partner/server/internal/module/user/repository"
2526
user_usecase "github.com/Financial-Partner/server/internal/module/user/usecase"
@@ -58,6 +59,14 @@ func ProvideUserService(repo user_repository.Repository, store *perRedis.UserSto
5859
return user_usecase.NewService(repo, store, log)
5960
}
6061

62+
func ProvideTransactionRepository(db *dbInfra.Client) transaction_repository.Repository {
63+
return perMongo.NewTransactionRepository(db)
64+
}
65+
66+
func ProvideTransactionStore(cache *cacheInfra.Client) *perRedis.TransactionStore {
67+
return perRedis.NewTransactionStore(cache)
68+
}
69+
6170
func ProvideJWTManager(cfg *config.Config) *authInfra.JWTManager {
6271
return authInfra.NewJWTManager(cfg.JWT.SecretKey, cfg.JWT.AccessExpiry, cfg.JWT.RefreshExpiry)
6372
}
@@ -84,8 +93,8 @@ func ProvideInvestmentService() *investment_usecase.Service {
8493
return investment_usecase.NewService()
8594
}
8695

87-
func ProvideTransactionService() *transaction_usecase.Service {
88-
return transaction_usecase.NewService()
96+
func ProvideTransactionService(repo transaction_repository.Repository, store *perRedis.TransactionStore, log loggerInfra.Logger) *transaction_usecase.Service {
97+
return transaction_usecase.NewService(repo, store, log)
8998
}
9099

91100
func ProvideGachaService() *gacha_usecase.Service {

cmd/server/wire.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ func InitializeServer(cfgFile string) (*Server, error) {
2323
ProvideAuthService,
2424
ProvideGoalService,
2525
ProvideInvestmentService,
26+
ProvideTransactionRepository,
27+
ProvideTransactionStore,
2628
ProvideTransactionService,
2729
ProvideGachaService,
2830
ProvideReportService,

cmd/server/wire_gen.go

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/infrastructure/auth/dummy.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package auth
33
import (
44
"errors"
55

6+
"go.mongodb.org/mongo-driver/bson/primitive"
7+
68
"github.com/Financial-Partner/server/internal/config"
79
)
810

@@ -21,8 +23,13 @@ func (v *DummyJWTValidator) ValidateToken(tokenString string) (*Claims, error) {
2123
return nil, errors.New("invalid token")
2224
}
2325

26+
dummyObjectID, err := primitive.ObjectIDFromHex("680b4fc122fc6fd9212d78f9")
27+
if err != nil {
28+
return nil, errors.New("failed to create dummy ObjectID")
29+
}
30+
2431
return &Claims{
25-
ID: "test-id",
32+
ID: dummyObjectID.Hex(),
2633
Email: "bypass@example.com",
2734
}, nil
2835
}

internal/infrastructure/persistence/mongodb/transaction.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55

66
"go.mongodb.org/mongo-driver/bson"
7+
"go.mongodb.org/mongo-driver/bson/primitive"
78
"go.mongodb.org/mongo-driver/mongo"
89

910
"github.com/Financial-Partner/server/internal/entities"
@@ -21,14 +22,15 @@ func NewTransactionRepository(db MongoClient) transaction_repository.Repository
2122
}
2223

2324
func (r *MongoTransactionRepository) Create(ctx context.Context, entity *entities.Transaction) (*entities.Transaction, error) {
25+
entity.ID = primitive.NewObjectID()
2426
_, err := r.collection.InsertOne(ctx, entity)
2527
if err != nil {
2628
return nil, err
2729
}
2830
return entity, nil
2931
}
3032

31-
func (r *MongoTransactionRepository) FindByUserId(ctx context.Context, userID string) ([]entities.Transaction, error) {
33+
func (r *MongoTransactionRepository) FindByUserId(ctx context.Context, userID primitive.ObjectID) ([]entities.Transaction, error) {
3234
var transactions []entities.Transaction
3335
cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID})
3436
if err != nil {

internal/infrastructure/persistence/mongodb/transaction_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717
func TestMongoTransactionRepository(t *testing.T) {
1818
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
1919

20-
testUserID := primitive.NewObjectID().Hex()
20+
testUserID := primitive.NewObjectID()
2121
testTransactions := []entities.Transaction{
2222
{
2323
ID: primitive.NewObjectID(),

internal/infrastructure/persistence/redis/transaction.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ package redis
22

33
import (
44
"context"
5-
"encoding/json"
5+
"errors"
66
"fmt"
77
"time"
88

99
"github.com/Financial-Partner/server/internal/entities"
10+
"github.com/redis/go-redis/v9"
1011
)
1112

1213
const (
@@ -31,13 +32,18 @@ func (s *TransactionStore) GetByUserId(ctx context.Context, userID string) ([]en
3132
return transactions, nil
3233
}
3334

34-
func (s *TransactionStore) SetByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error {
35-
data, err := json.Marshal(transactions)
36-
if err != nil {
35+
func (s *TransactionStore) SetMultipleByUserId(ctx context.Context, userID string, transactions []entities.Transaction) error {
36+
existingTransactions, err := s.GetByUserId(ctx, userID)
37+
if err != nil && !errors.Is(err, redis.Nil) {
3738
return err
3839
}
3940

40-
return s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), data, transactionCacheTTL)
41+
transactions = append(existingTransactions, transactions...)
42+
err = s.cacheClient.Set(ctx, fmt.Sprintf(transactionCacheKey, userID), transactions, transactionCacheTTL)
43+
if err != nil {
44+
return err
45+
}
46+
return nil
4147
}
4248

4349
func (s *TransactionStore) DeleteByUserId(ctx context.Context, userID string) error {

internal/infrastructure/persistence/redis/transaction_test.go

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ func TestTransactionStore(t *testing.T) {
2525
transactionStore := redis.NewTransactionStore(mockRedisClient)
2626

2727
// Mock data
28-
userID := primitive.NewObjectID().Hex()
28+
userID := primitive.NewObjectID()
2929

3030
mockTransactions := []entities.Transaction{
3131
{
3232
ID: primitive.NewObjectID(),
33-
UserID: primitive.NewObjectID(),
33+
UserID: userID,
3434
Amount: 100,
3535
Description: "Groceries",
3636
Date: time.Now(),
@@ -41,7 +41,7 @@ func TestTransactionStore(t *testing.T) {
4141
},
4242
{
4343
ID: primitive.NewObjectID(),
44-
UserID: primitive.NewObjectID(),
44+
UserID: userID,
4545
Amount: 200,
4646
Description: "Rent",
4747
Date: time.Now(),
@@ -56,12 +56,12 @@ func TestTransactionStore(t *testing.T) {
5656
mockData, _ := json.Marshal(mockTransactions)
5757

5858
// 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(
59+
mockRedisClient.EXPECT().Get(gomock.Any(), fmt.Sprintf("user:%s:transactions", userID.Hex()), gomock.Any()).DoAndReturn(
6060
func(_ context.Context, _ string, dest interface{}) error {
6161
return json.Unmarshal(mockData, dest)
6262
},
6363
)
64-
transactions, err := transactionStore.GetByUserId(context.Background(), userID)
64+
transactions, err := transactionStore.GetByUserId(context.Background(), userID.Hex())
6565
require.NoError(t, err)
6666
assert.NotNil(t, transactions)
6767
})
@@ -70,24 +70,24 @@ func TestTransactionStore(t *testing.T) {
7070
mockRedisClient := redis.NewMockRedisClient(ctrl)
7171
transactionStore := redis.NewTransactionStore(mockRedisClient)
7272

73-
userID := primitive.NewObjectID().Hex()
73+
userID := primitive.NewObjectID()
7474
mockRedisClient.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(goredis.Nil)
7575

76-
transactions, err := transactionStore.GetByUserId(context.Background(), userID)
76+
transactions, err := transactionStore.GetByUserId(context.Background(), userID.Hex())
7777
require.Error(t, err)
7878
assert.Nil(t, transactions)
7979
})
8080

81-
t.Run("SetByUserIdSuccess", func(t *testing.T) {
81+
t.Run("SetMultipleByUserIdWithExistingTransactions", func(t *testing.T) {
8282
mockRedisClient := redis.NewMockRedisClient(ctrl)
8383
transactionStore := redis.NewTransactionStore(mockRedisClient)
8484

85-
// mock data
86-
userID := primitive.NewObjectID().Hex()
87-
mockTransactions := []entities.Transaction{
85+
// Mock data
86+
userID := primitive.NewObjectID()
87+
existingTransactions := []entities.Transaction{
8888
{
8989
ID: primitive.NewObjectID(),
90-
UserID: primitive.NewObjectID(),
90+
UserID: userID,
9191
Amount: 100,
9292
Description: "Groceries",
9393
Date: time.Now(),
@@ -96,9 +96,11 @@ func TestTransactionStore(t *testing.T) {
9696
CreatedAt: time.Now(),
9797
UpdatedAt: time.Now(),
9898
},
99+
}
100+
newTransactions := []entities.Transaction{
99101
{
100102
ID: primitive.NewObjectID(),
101-
UserID: primitive.NewObjectID(),
103+
UserID: userID,
102104
Amount: 200,
103105
Description: "Rent",
104106
Date: time.Now(),
@@ -107,21 +109,58 @@ func TestTransactionStore(t *testing.T) {
107109
CreatedAt: time.Now(),
108110
UpdatedAt: time.Now(),
109111
},
112+
{
113+
ID: primitive.NewObjectID(),
114+
UserID: userID,
115+
Amount: 50,
116+
Description: "Utilities",
117+
Date: time.Now(),
118+
Category: "Bills",
119+
Type: "expense",
120+
CreatedAt: time.Now(),
121+
UpdatedAt: time.Now(),
122+
},
110123
}
111-
mockRedisClient.EXPECT().Set(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
112124

113-
err := transactionStore.SetByUserId(context.Background(), userID, mockTransactions)
125+
// Serialize existing transactions to JSON
126+
mockData, _ := json.Marshal(existingTransactions)
127+
128+
// Mock the Get method to return the existing transactions
129+
mockRedisClient.EXPECT().Get(
130+
gomock.Any(),
131+
fmt.Sprintf("user:%s:transactions", userID.Hex()),
132+
gomock.Any(),
133+
).DoAndReturn(
134+
func(_ context.Context, _ string, dest interface{}) error {
135+
return json.Unmarshal(mockData, dest)
136+
},
137+
)
138+
139+
// Mock the Set method to save the updated transactions
140+
mockRedisClient.EXPECT().Set(
141+
gomock.Any(),
142+
fmt.Sprintf("user:%s:transactions", userID.Hex()),
143+
gomock.Any(),
144+
gomock.Any(),
145+
).DoAndReturn(
146+
func(_ context.Context, _ string, value interface{}, _ time.Duration) error {
147+
return nil
148+
},
149+
)
150+
151+
// Call SetMultipleByUserId
152+
err := transactionStore.SetMultipleByUserId(context.Background(), userID.Hex(), newTransactions)
114153
require.NoError(t, err)
115154
})
116155

117156
t.Run("DeleteTransactionSuccess", func(t *testing.T) {
118157
mockRedisClient := redis.NewMockRedisClient(ctrl)
119158
transactionStore := redis.NewTransactionStore(mockRedisClient)
120159

121-
userID := primitive.NewObjectID().Hex()
122-
mockRedisClient.EXPECT().Delete(gomock.Any(), fmt.Sprintf("user:%s:transactions", userID)).Return(nil)
160+
userID := primitive.NewObjectID()
161+
mockRedisClient.EXPECT().Delete(gomock.Any(), fmt.Sprintf("user:%s:transactions", userID.Hex())).Return(nil)
123162

124-
err := transactionStore.DeleteByUserId(context.Background(), userID)
163+
err := transactionStore.DeleteByUserId(context.Background(), userID.Hex())
125164
require.NoError(t, err)
126165
})
127166
}

internal/interfaces/http/report.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/Financial-Partner/server/internal/entities"
1111
"github.com/Financial-Partner/server/internal/interfaces/http/dto"
1212
httperror "github.com/Financial-Partner/server/internal/interfaces/http/error"
13-
responde "github.com/Financial-Partner/server/internal/interfaces/http/respond"
13+
respond "github.com/Financial-Partner/server/internal/interfaces/http/respond"
1414
)
1515

1616
//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) {
4646
startTimestamp, err := strconv.ParseInt(start, 10, 64) // Parse Unix timestamp
4747
if err != nil {
4848
h.log.Warnf("Invalid start timestamp format. Use a valid Unix timestamp.")
49-
responde.WithError(w, r, h.log, err, httperror.ErrInvalidParameter, http.StatusBadRequest)
49+
respond.WithError(w, r, h.log, err, httperror.ErrInvalidParameter, http.StatusBadRequest)
5050
return
5151
}
5252
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) {
5656
endTimestamp, err := strconv.ParseInt(end, 10, 64) // Parse Unix timestamp
5757
if err != nil {
5858
h.log.Warnf("Invalid end timestamp format. Use a valid Unix timestamp.")
59-
responde.WithError(w, r, h.log, err, httperror.ErrInvalidParameter, http.StatusBadRequest)
59+
respond.WithError(w, r, h.log, err, httperror.ErrInvalidParameter, http.StatusBadRequest)
6060
return
6161
}
6262
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) {
6565
userID, ok := contextutil.GetUserID(r.Context())
6666
if !ok {
6767
h.log.Warnf("failed to get user ID from context")
68-
responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized)
68+
respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized)
6969
return
7070
}
7171

7272
report, err := h.reportService.GetReport(r.Context(), userID, startDate, endDate, reportType)
7373
if err != nil {
7474
h.log.Errorf("failed to get report")
75-
responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetReport, http.StatusInternalServerError)
75+
respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetReport, http.StatusInternalServerError)
7676
return
7777
}
7878

@@ -85,7 +85,7 @@ func (h *Handler) GetReport(w http.ResponseWriter, r *http.Request) {
8585
Percentages: report.Percentages,
8686
}
8787

88-
responde.WithJSON(w, r, resp, http.StatusOK)
88+
respond.WithJSON(w, r, resp, http.StatusOK)
8989
}
9090

9191
// @Summary Get report summary
@@ -102,20 +102,20 @@ func (h *Handler) GetReportSummary(w http.ResponseWriter, r *http.Request) {
102102
userID, ok := contextutil.GetUserID(r.Context())
103103
if !ok {
104104
h.log.Warnf("failed to get user ID from context")
105-
responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized)
105+
respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized)
106106
return
107107
}
108108

109109
reportSummary, err := h.reportService.GetReportSummary(r.Context(), userID)
110110
if err != nil {
111111
h.log.Errorf("failed to get report summary")
112-
responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetReportSummary, http.StatusInternalServerError)
112+
respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetReportSummary, http.StatusInternalServerError)
113113
return
114114
}
115115

116116
resp := dto.ReportSummaryResponse{
117117
Summary: reportSummary.Summary,
118118
}
119119

120-
responde.WithJSON(w, r, resp, http.StatusOK)
120+
respond.WithJSON(w, r, resp, http.StatusOK)
121121
}

internal/interfaces/http/transaction.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ func (h *Handler) CreateTransaction(w http.ResponseWriter, r *http.Request) {
9494

9595
transaction, err := h.transactionService.CreateTransaction(r.Context(), userID, &req)
9696
if err != nil {
97-
h.log.Errorf("failed to create transaction")
97+
h.log.Errorf("failed to create transaction: %v", err)
9898
respond.WithError(w, r, h.log, err, httperror.ErrFailedToCreateTransaction, http.StatusInternalServerError)
9999
return
100100
}

0 commit comments

Comments
 (0)