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
1 change: 1 addition & 0 deletions cmd/server/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func setupProtectedRoutes(router *mux.Router, handlers *handler.Handler) {

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

userInvestmentRoutes := router.PathPrefix("/users/me/investment").Subrouter()
userInvestmentRoutes.HandleFunc("/", handlers.CreateUserInvestment).Methods(http.MethodPost)
Expand Down
34 changes: 18 additions & 16 deletions internal/entities/goal.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,31 @@ package entities

import (
"time"

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

type GoalSuggestion struct {
SuggestedAmount int64
Period int
Message string
SuggestedAmount int64 `bson:"suggested_amount" json:"suggested_amount"`
Period int `bson:"period" json:"period"`
Message string `bson:"message" json:"message"`
}

type Goal struct {
ID string
UserID string
TargetAmount int64
CurrentAmount int64
Period int
Status string
CreatedAt time.Time
UpdatedAt time.Time
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
UserID primitive.ObjectID `bson:"user_id" json:"user_id"`
TargetAmount int64 `bson:"target_amount" json:"target_amount"`
CurrentAmount int64 `bson:"current_amount" json:"current_amount"`
Period int `bson:"period" json:"period"`
Status string `bson:"status" json:"status"` // "active", "completed", "failed"
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}

type GoalMilestone struct {
Title string
TargetPercent int
Reward string
IsCompleted bool
CompletedAt *time.Time
Title string `bson:"title" json:"title"`
TargetPercent int `bson:"target_percent" json:"target_percent"`
Reward string `bson:"reward" json:"reward"`
IsCompleted bool `bson:"is_completed" json:"is_completed"`
CompletedAt *time.Time `bson:"completed_at" json:"completed_at"`
}
34 changes: 18 additions & 16 deletions internal/entities/investment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,28 @@ package entities

import (
"time"

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

type Opportunity struct {
ID string `bson:"_id,omitempty" json:"id"`
Title string `bson:"title" json:"title"`
Description string `bson:"description" json:"description"`
Tags []string `bson:"tags" json:"tags"`
IsIncrease bool `bson:"is_increase" json:"is_increase"`
Variation int64 `bson:"variation" json:"variation"`
Duration string `bson:"duration" json:"duration"`
MinAmount int64 `bson:"min_amount" json:"min_amount"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
Title string `bson:"title" json:"title"`
Description string `bson:"description" json:"description"`
Tags []string `bson:"tags" json:"tags"`
IsIncrease bool `bson:"is_increase" json:"is_increase"`
Variation int64 `bson:"variation" json:"variation"`
Duration string `bson:"duration" json:"duration"`
MinAmount int64 `bson:"min_amount" json:"min_amount"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}

type Investment struct {
ID string `bson:"_id,omitempty" json:"id"`
UserID string `bson:"user_id" json:"user_id"`
OpportunityID string `bson:"opportunity_id" json:"opportunity_id"`
Amount int64 `bson:"amount" json:"amount"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
UserID primitive.ObjectID `bson:"user_id" json:"user_id"`
OpportunityID primitive.ObjectID `bson:"opportunity_id" json:"opportunity_id"`
Amount int64 `bson:"amount" json:"amount"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
2 changes: 1 addition & 1 deletion internal/entities/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (

type Transaction struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
UserID string `bson:"user_id" json:"user_id"`
UserID primitive.ObjectID `bson:"user_id" json:"user_id"`
Amount int `bson:"amount" json:"amount"`
Description string `bson:"description" json:"description"`
Date time.Time `bson:"date" json:"date"`
Expand Down
66 changes: 66 additions & 0 deletions internal/infrastructure/persistence/mongodb/investment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package mongodb

import (
"context"

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

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

type MongoInvestmentRepository struct {
collection *mongo.Collection
}

func NewInvestmentRepository(db MongoClient) *MongoInvestmentRepository {
return &MongoInvestmentRepository{
collection: db.Collection("investments"),
}
}

func (r *MongoInvestmentRepository) CreateInvestment(ctx context.Context, entity *entities.Investment) (*entities.Investment, error) {
_, err := r.collection.InsertOne(ctx, entity)
if err != nil {
return nil, err
}
return entity, nil
}

func (r *MongoInvestmentRepository) CreateOpportunity(ctx context.Context, entity *entities.Opportunity) (*entities.Opportunity, error) {
_, err := r.collection.InsertOne(ctx, entity)
if err != nil {
return nil, err
}
return entity, nil
}

func (r *MongoInvestmentRepository) FindOpportunitiesByUserId(ctx context.Context, userID string) ([]entities.Opportunity, error) {
var opportunities []entities.Opportunity
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, &opportunities); err != nil {
return nil, err
}

return opportunities, nil
}

func (r *MongoInvestmentRepository) FindInvestmentsByUserId(ctx context.Context, userID string) ([]entities.Investment, error) {
var investments []entities.Investment
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, &investments); err != nil {
return nil, err
}

return investments, nil
}
200 changes: 200 additions & 0 deletions internal/infrastructure/persistence/mongodb/investment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
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"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
)

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

testUserID := primitive.NewObjectID().Hex()

testInvestment := &entities.Investment{
ID: primitive.NewObjectID(),
UserID: primitive.NewObjectID(),
OpportunityID: primitive.NewObjectID(),
Amount: 1000,
CreatedAt: time.Date(2023, time.January, 31, 0, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2023, time.January, 31, 0, 0, 0, 0, time.UTC),
}
testInvestments := []entities.Investment{
*testInvestment,
}

testOpportunity := &entities.Opportunity{
ID: primitive.NewObjectID(),
Title: "real estate",
Description: "Invest in real estate",
Tags: []string{"high risk", "long term"},
IsIncrease: true,
Variation: 10,
Duration: "1 year",
MinAmount: 1000,
CreatedAt: time.Date(2023, time.January, 31, 0, 0, 0, 0, time.UTC),
UpdatedAt: time.Date(2023, time.January, 31, 0, 0, 0, 0, time.UTC),
}
testOpportunities := []entities.Opportunity{
*testOpportunity,
}

var testInvestmentDocs []bson.D
for _, investment := range testInvestments {
investmentBSON, err := bson.Marshal(investment)
assert.NoError(t, err)
var investmentDoc bson.D
err = bson.Unmarshal(investmentBSON, &investmentDoc)
assert.NoError(t, err)
testInvestmentDocs = append(testInvestmentDocs, investmentDoc)
}

var testOpportunityDocs []bson.D
for _, opportunity := range testOpportunities {
opportunityBSON, err := bson.Marshal(opportunity)
assert.NoError(t, err)
var opportunityDoc bson.D
err = bson.Unmarshal(opportunityBSON, &opportunityDoc)
assert.NoError(t, err)
testOpportunityDocs = append(testOpportunityDocs, opportunityDoc)
}

t.Run("CreateInvestment", func(t *testing.T) {
mt.Run("error", func(mt *mtest.T) {
mt.AddMockResponses(
mtest.CreateCommandErrorResponse(mtest.CommandError{
Code: 11000,
Message: "Duplicate key error",
}),
)

repo := mongodb.NewInvestmentRepository(mt.DB)
result, err := repo.CreateInvestment(context.Background(), testInvestment)
assert.Error(t, err)
assert.Nil(t, result)
})

mt.Run("success", func(mt *mtest.T) {
mt.AddMockResponses(
mtest.CreateSuccessResponse(),
)

repo := mongodb.NewInvestmentRepository(mt.DB)
result, err := repo.CreateInvestment(context.Background(), testInvestment)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, testInvestment, result)
})
})

t.Run("CreateOpportunity", func(t *testing.T) {
mt.Run("error", func(mt *mtest.T) {
mt.AddMockResponses(
mtest.CreateCommandErrorResponse(mtest.CommandError{
Code: 11000,
Message: "Duplicate key error",
}),
)

repo := mongodb.NewInvestmentRepository(mt.DB)
result, err := repo.CreateOpportunity(context.Background(), testOpportunity)
assert.Error(t, err)
assert.Nil(t, result)
})

mt.Run("success", func(mt *mtest.T) {
mt.AddMockResponses(
mtest.CreateSuccessResponse(),
)

repo := mongodb.NewInvestmentRepository(mt.DB)
result, err := repo.CreateOpportunity(context.Background(), testOpportunity)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, testOpportunity, result)
})
})

t.Run("FindOpportunitiesByUserId", func(t *testing.T) {
mt.Run("database error", func(mt *mtest.T) {
mt.AddMockResponses(
mtest.CreateCommandErrorResponse(mtest.CommandError{
Code: 11000,
Message: "Database error",
}),
)

repo := mongodb.NewInvestmentRepository(mt.DB)
result, err := repo.FindOpportunitiesByUserId(context.Background(), testUserID)
assert.Error(t, err)
assert.Nil(t, result)
})
mt.Run("not found", func(mt *mtest.T) {
mt.AddMockResponses(mtest.CreateCursorResponse(0, "foo.bar", mtest.FirstBatch))
repo := mongodb.NewInvestmentRepository(mt.DB)
result, err := repo.FindOpportunitiesByUserId(context.Background(), testUserID)
assert.NoError(t, err)
assert.Nil(t, result)
})
mt.Run("success", func(mt *mtest.T) {
mt.AddMockResponses(
mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, testOpportunityDocs...),
mtest.CreateCursorResponse(0, "foo.bar", mtest.NextBatch),
)
repo := mongodb.NewInvestmentRepository(mt.Client.Database("testdb"))
result, err := repo.FindOpportunitiesByUserId(context.Background(), testUserID)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Len(t, result, len(testOpportunityDocs))
// Validate each investment
for i, opportunity := range result {
assert.Equal(t, testOpportunities[i], opportunity)
}
})
})

t.Run("FindInvestmentsByUserId", func(t *testing.T) {
mt.Run("database error", func(mt *mtest.T) {
mt.AddMockResponses(
mtest.CreateCommandErrorResponse(mtest.CommandError{
Code: 11000,
Message: "Database error",
}),
)

repo := mongodb.NewInvestmentRepository(mt.DB)
result, err := repo.FindInvestmentsByUserId(context.Background(), testUserID)
assert.Error(t, err)
assert.Nil(t, result)
})
mt.Run("not found", func(mt *mtest.T) {
mt.AddMockResponses(mtest.CreateCursorResponse(0, "foo.bar", mtest.FirstBatch))
repo := mongodb.NewInvestmentRepository(mt.DB)
result, err := repo.FindInvestmentsByUserId(context.Background(), testUserID)
assert.NoError(t, err)
assert.Nil(t, result)
})
mt.Run("success", func(mt *mtest.T) {
mt.AddMockResponses(
mtest.CreateCursorResponse(1, "foo.bar", mtest.FirstBatch, testInvestmentDocs...),
mtest.CreateCursorResponse(0, "foo.bar", mtest.NextBatch),
)
repo := mongodb.NewInvestmentRepository(mt.Client.Database("testdb"))
result, err := repo.FindInvestmentsByUserId(context.Background(), testUserID)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Len(t, result, len(testInvestmentDocs))
// Validate each investment
for i, investment := range result {
assert.Equal(t, testInvestments[i], investment)
}
})
})
}
Loading
Loading