Skip to content

Commit 0d31314

Browse files
authored
Merge pull request #16 from Financial-Partner/feat/implement_investment_service
Implement Repository and InvestmentStore Interfaces for Database and Cache Operations
2 parents c6a019b + a375767 commit 0d31314

34 files changed

Lines changed: 1489 additions & 218 deletions

cmd/server/route.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ func setupProtectedRoutes(router *mux.Router, handlers *handler.Handler) {
6161

6262
investmentRoutes := router.PathPrefix("/investments").Subrouter()
6363
investmentRoutes.HandleFunc("", handlers.GetOpportunities).Methods(http.MethodGet)
64+
investmentRoutes.HandleFunc("", handlers.CreateOpportunity).Methods(http.MethodPost)
6465

6566
userInvestmentRoutes := router.PathPrefix("/users/me/investment").Subrouter()
6667
userInvestmentRoutes.HandleFunc("/", handlers.CreateUserInvestment).Methods(http.MethodPost)

internal/entities/goal.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,31 @@ package entities
22

33
import (
44
"time"
5+
6+
"go.mongodb.org/mongo-driver/bson/primitive"
57
)
68

79
type GoalSuggestion struct {
8-
SuggestedAmount int64
9-
Period int
10-
Message string
10+
SuggestedAmount int64 `bson:"suggested_amount" json:"suggested_amount"`
11+
Period int `bson:"period" json:"period"`
12+
Message string `bson:"message" json:"message"`
1113
}
1214

1315
type Goal struct {
14-
ID string
15-
UserID string
16-
TargetAmount int64
17-
CurrentAmount int64
18-
Period int
19-
Status string
20-
CreatedAt time.Time
21-
UpdatedAt time.Time
16+
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
17+
UserID primitive.ObjectID `bson:"user_id" json:"user_id"`
18+
TargetAmount int64 `bson:"target_amount" json:"target_amount"`
19+
CurrentAmount int64 `bson:"current_amount" json:"current_amount"`
20+
Period int `bson:"period" json:"period"`
21+
Status string `bson:"status" json:"status"` // "active", "completed", "failed"
22+
CreatedAt time.Time `bson:"created_at" json:"created_at"`
23+
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
2224
}
2325

2426
type GoalMilestone struct {
25-
Title string
26-
TargetPercent int
27-
Reward string
28-
IsCompleted bool
29-
CompletedAt *time.Time
27+
Title string `bson:"title" json:"title"`
28+
TargetPercent int `bson:"target_percent" json:"target_percent"`
29+
Reward string `bson:"reward" json:"reward"`
30+
IsCompleted bool `bson:"is_completed" json:"is_completed"`
31+
CompletedAt *time.Time `bson:"completed_at" json:"completed_at"`
3032
}

internal/entities/investment.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,28 @@ package entities
22

33
import (
44
"time"
5+
6+
"go.mongodb.org/mongo-driver/bson/primitive"
57
)
68

79
type Opportunity struct {
8-
ID string `bson:"_id,omitempty" json:"id"`
9-
Title string `bson:"title" json:"title"`
10-
Description string `bson:"description" json:"description"`
11-
Tags []string `bson:"tags" json:"tags"`
12-
IsIncrease bool `bson:"is_increase" json:"is_increase"`
13-
Variation int64 `bson:"variation" json:"variation"`
14-
Duration string `bson:"duration" json:"duration"`
15-
MinAmount int64 `bson:"min_amount" json:"min_amount"`
16-
CreatedAt time.Time `bson:"created_at" json:"created_at"`
17-
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
10+
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
11+
Title string `bson:"title" json:"title"`
12+
Description string `bson:"description" json:"description"`
13+
Tags []string `bson:"tags" json:"tags"`
14+
IsIncrease bool `bson:"is_increase" json:"is_increase"`
15+
Variation int64 `bson:"variation" json:"variation"`
16+
Duration string `bson:"duration" json:"duration"`
17+
MinAmount int64 `bson:"min_amount" json:"min_amount"`
18+
CreatedAt time.Time `bson:"created_at" json:"created_at"`
19+
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
1820
}
1921

2022
type Investment struct {
21-
ID string `bson:"_id,omitempty" json:"id"`
22-
UserID string `bson:"user_id" json:"user_id"`
23-
OpportunityID string `bson:"opportunity_id" json:"opportunity_id"`
24-
Amount int64 `bson:"amount" json:"amount"`
25-
CreatedAt time.Time `bson:"created_at" json:"created_at"`
26-
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
23+
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
24+
UserID primitive.ObjectID `bson:"user_id" json:"user_id"`
25+
OpportunityID primitive.ObjectID `bson:"opportunity_id" json:"opportunity_id"`
26+
Amount int64 `bson:"amount" json:"amount"`
27+
CreatedAt time.Time `bson:"created_at" json:"created_at"`
28+
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
2729
}

internal/entities/transaction.go

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

99
type Transaction struct {
1010
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
11-
UserID string `bson:"user_id" json:"user_id"`
11+
UserID primitive.ObjectID `bson:"user_id" json:"user_id"`
1212
Amount int `bson:"amount" json:"amount"`
1313
Description string `bson:"description" json:"description"`
1414
Date time.Time `bson:"date" json:"date"`
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
)
11+
12+
type MongoInvestmentRepository struct {
13+
collection *mongo.Collection
14+
}
15+
16+
func NewInvestmentRepository(db MongoClient) *MongoInvestmentRepository {
17+
return &MongoInvestmentRepository{
18+
collection: db.Collection("investments"),
19+
}
20+
}
21+
22+
func (r *MongoInvestmentRepository) CreateInvestment(ctx context.Context, entity *entities.Investment) (*entities.Investment, error) {
23+
_, err := r.collection.InsertOne(ctx, entity)
24+
if err != nil {
25+
return nil, err
26+
}
27+
return entity, nil
28+
}
29+
30+
func (r *MongoInvestmentRepository) CreateOpportunity(ctx context.Context, entity *entities.Opportunity) (*entities.Opportunity, error) {
31+
_, err := r.collection.InsertOne(ctx, entity)
32+
if err != nil {
33+
return nil, err
34+
}
35+
return entity, nil
36+
}
37+
38+
func (r *MongoInvestmentRepository) FindOpportunitiesByUserId(ctx context.Context, userID string) ([]entities.Opportunity, error) {
39+
var opportunities []entities.Opportunity
40+
cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID})
41+
if err != nil {
42+
return nil, err
43+
}
44+
defer cursor.Close(ctx)
45+
46+
if err := cursor.All(ctx, &opportunities); err != nil {
47+
return nil, err
48+
}
49+
50+
return opportunities, nil
51+
}
52+
53+
func (r *MongoInvestmentRepository) FindInvestmentsByUserId(ctx context.Context, userID string) ([]entities.Investment, error) {
54+
var investments []entities.Investment
55+
cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID})
56+
if err != nil {
57+
return nil, err
58+
}
59+
defer cursor.Close(ctx)
60+
61+
if err := cursor.All(ctx, &investments); err != nil {
62+
return nil, err
63+
}
64+
65+
return investments, nil
66+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
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+
"go.mongodb.org/mongo-driver/bson"
12+
"go.mongodb.org/mongo-driver/bson/primitive"
13+
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
14+
)
15+
16+
func TestMongoInvestmentRepository(t *testing.T) {
17+
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
18+
19+
testUserID := primitive.NewObjectID().Hex()
20+
21+
testInvestment := &entities.Investment{
22+
ID: primitive.NewObjectID(),
23+
UserID: primitive.NewObjectID(),
24+
OpportunityID: primitive.NewObjectID(),
25+
Amount: 1000,
26+
CreatedAt: time.Date(2023, time.January, 31, 0, 0, 0, 0, time.UTC),
27+
UpdatedAt: time.Date(2023, time.January, 31, 0, 0, 0, 0, time.UTC),
28+
}
29+
testInvestments := []entities.Investment{
30+
*testInvestment,
31+
}
32+
33+
testOpportunity := &entities.Opportunity{
34+
ID: primitive.NewObjectID(),
35+
Title: "real estate",
36+
Description: "Invest in real estate",
37+
Tags: []string{"high risk", "long term"},
38+
IsIncrease: true,
39+
Variation: 10,
40+
Duration: "1 year",
41+
MinAmount: 1000,
42+
CreatedAt: time.Date(2023, time.January, 31, 0, 0, 0, 0, time.UTC),
43+
UpdatedAt: time.Date(2023, time.January, 31, 0, 0, 0, 0, time.UTC),
44+
}
45+
testOpportunities := []entities.Opportunity{
46+
*testOpportunity,
47+
}
48+
49+
var testInvestmentDocs []bson.D
50+
for _, investment := range testInvestments {
51+
investmentBSON, err := bson.Marshal(investment)
52+
assert.NoError(t, err)
53+
var investmentDoc bson.D
54+
err = bson.Unmarshal(investmentBSON, &investmentDoc)
55+
assert.NoError(t, err)
56+
testInvestmentDocs = append(testInvestmentDocs, investmentDoc)
57+
}
58+
59+
var testOpportunityDocs []bson.D
60+
for _, opportunity := range testOpportunities {
61+
opportunityBSON, err := bson.Marshal(opportunity)
62+
assert.NoError(t, err)
63+
var opportunityDoc bson.D
64+
err = bson.Unmarshal(opportunityBSON, &opportunityDoc)
65+
assert.NoError(t, err)
66+
testOpportunityDocs = append(testOpportunityDocs, opportunityDoc)
67+
}
68+
69+
t.Run("CreateInvestment", func(t *testing.T) {
70+
mt.Run("error", func(mt *mtest.T) {
71+
mt.AddMockResponses(
72+
mtest.CreateCommandErrorResponse(mtest.CommandError{
73+
Code: 11000,
74+
Message: "Duplicate key error",
75+
}),
76+
)
77+
78+
repo := mongodb.NewInvestmentRepository(mt.DB)
79+
result, err := repo.CreateInvestment(context.Background(), testInvestment)
80+
assert.Error(t, err)
81+
assert.Nil(t, result)
82+
})
83+
84+
mt.Run("success", func(mt *mtest.T) {
85+
mt.AddMockResponses(
86+
mtest.CreateSuccessResponse(),
87+
)
88+
89+
repo := mongodb.NewInvestmentRepository(mt.DB)
90+
result, err := repo.CreateInvestment(context.Background(), testInvestment)
91+
assert.NoError(t, err)
92+
assert.NotNil(t, result)
93+
assert.Equal(t, testInvestment, result)
94+
})
95+
})
96+
97+
t.Run("CreateOpportunity", func(t *testing.T) {
98+
mt.Run("error", func(mt *mtest.T) {
99+
mt.AddMockResponses(
100+
mtest.CreateCommandErrorResponse(mtest.CommandError{
101+
Code: 11000,
102+
Message: "Duplicate key error",
103+
}),
104+
)
105+
106+
repo := mongodb.NewInvestmentRepository(mt.DB)
107+
result, err := repo.CreateOpportunity(context.Background(), testOpportunity)
108+
assert.Error(t, err)
109+
assert.Nil(t, result)
110+
})
111+
112+
mt.Run("success", func(mt *mtest.T) {
113+
mt.AddMockResponses(
114+
mtest.CreateSuccessResponse(),
115+
)
116+
117+
repo := mongodb.NewInvestmentRepository(mt.DB)
118+
result, err := repo.CreateOpportunity(context.Background(), testOpportunity)
119+
assert.NoError(t, err)
120+
assert.NotNil(t, result)
121+
assert.Equal(t, testOpportunity, result)
122+
})
123+
})
124+
125+
t.Run("FindOpportunitiesByUserId", func(t *testing.T) {
126+
mt.Run("database error", func(mt *mtest.T) {
127+
mt.AddMockResponses(
128+
mtest.CreateCommandErrorResponse(mtest.CommandError{
129+
Code: 11000,
130+
Message: "Database error",
131+
}),
132+
)
133+
134+
repo := mongodb.NewInvestmentRepository(mt.DB)
135+
result, err := repo.FindOpportunitiesByUserId(context.Background(), testUserID)
136+
assert.Error(t, err)
137+
assert.Nil(t, result)
138+
})
139+
mt.Run("not found", func(mt *mtest.T) {
140+
mt.AddMockResponses(mtest.CreateCursorResponse(0, "foo.bar", mtest.FirstBatch))
141+
repo := mongodb.NewInvestmentRepository(mt.DB)
142+
result, err := repo.FindOpportunitiesByUserId(context.Background(), testUserID)
143+
assert.NoError(t, err)
144+
assert.Nil(t, result)
145+
})
146+
mt.Run("success", func(mt *mtest.T) {
147+
mt.AddMockResponses(
148+
mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, testOpportunityDocs...),
149+
mtest.CreateCursorResponse(0, "foo.bar", mtest.NextBatch),
150+
)
151+
repo := mongodb.NewInvestmentRepository(mt.Client.Database("testdb"))
152+
result, err := repo.FindOpportunitiesByUserId(context.Background(), testUserID)
153+
assert.NoError(t, err)
154+
assert.NotNil(t, result)
155+
assert.Len(t, result, len(testOpportunityDocs))
156+
// Validate each investment
157+
for i, opportunity := range result {
158+
assert.Equal(t, testOpportunities[i], opportunity)
159+
}
160+
})
161+
})
162+
163+
t.Run("FindInvestmentsByUserId", func(t *testing.T) {
164+
mt.Run("database error", func(mt *mtest.T) {
165+
mt.AddMockResponses(
166+
mtest.CreateCommandErrorResponse(mtest.CommandError{
167+
Code: 11000,
168+
Message: "Database error",
169+
}),
170+
)
171+
172+
repo := mongodb.NewInvestmentRepository(mt.DB)
173+
result, err := repo.FindInvestmentsByUserId(context.Background(), testUserID)
174+
assert.Error(t, err)
175+
assert.Nil(t, result)
176+
})
177+
mt.Run("not found", func(mt *mtest.T) {
178+
mt.AddMockResponses(mtest.CreateCursorResponse(0, "foo.bar", mtest.FirstBatch))
179+
repo := mongodb.NewInvestmentRepository(mt.DB)
180+
result, err := repo.FindInvestmentsByUserId(context.Background(), testUserID)
181+
assert.NoError(t, err)
182+
assert.Nil(t, result)
183+
})
184+
mt.Run("success", func(mt *mtest.T) {
185+
mt.AddMockResponses(
186+
mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, testInvestmentDocs...),
187+
mtest.CreateCursorResponse(0, "foo.bar", mtest.NextBatch),
188+
)
189+
repo := mongodb.NewInvestmentRepository(mt.Client.Database("testdb"))
190+
result, err := repo.FindInvestmentsByUserId(context.Background(), testUserID)
191+
assert.NoError(t, err)
192+
assert.NotNil(t, result)
193+
assert.Len(t, result, len(testInvestmentDocs))
194+
// Validate each investment
195+
for i, investment := range result {
196+
assert.Equal(t, testInvestments[i], investment)
197+
}
198+
})
199+
})
200+
}

0 commit comments

Comments
 (0)