From 1beb697fde35ce8e54f1dc845158bb414f2cf91d Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Mon, 21 Apr 2025 01:29:28 +0800 Subject: [PATCH 1/4] Add investment domain interfaces and MongoDB/Redis repositories --- internal/entities/investment.go | 34 +-- .../persistence/mongodb/investment.go | 58 ++++++ .../persistence/mongodb/investment_test.go | 172 +++++++++++++++ .../persistence/mongodb/transaction_test.go | 25 +-- .../persistence/redis/investment.go | 68 ++++++ .../persistence/redis/investment_test.go | 161 ++++++++++++++ internal/interfaces/http/dto/investment.go | 2 - internal/interfaces/http/investment.go | 10 +- internal/interfaces/http/investment_test.go | 23 +- .../module/investment/domain/interfaces.go | 16 ++ .../investment/domain/interfaces_mock.go | 88 ++++++++ .../investment/repository/repository.go | 24 +++ .../investment/repository/repository_mock.go | 197 ++++++++++++++++++ .../transaction/repository/repository.go | 2 +- ..._repository_mock.go => repository_mock.go} | 2 +- swagger/docs.go | 8 - swagger/swagger.json | 8 - swagger/swagger.yaml | 6 - 18 files changed, 826 insertions(+), 78 deletions(-) create mode 100644 internal/infrastructure/persistence/mongodb/investment.go create mode 100644 internal/infrastructure/persistence/mongodb/investment_test.go create mode 100644 internal/infrastructure/persistence/redis/investment.go create mode 100644 internal/infrastructure/persistence/redis/investment_test.go create mode 100644 internal/module/investment/domain/interfaces.go create mode 100644 internal/module/investment/domain/interfaces_mock.go create mode 100644 internal/module/investment/repository/repository.go create mode 100644 internal/module/investment/repository/repository_mock.go rename internal/module/transaction/repository/{transaction_repository_mock.go => repository_mock.go} (97%) diff --git a/internal/entities/investment.go b/internal/entities/investment.go index c13edd5..904976c 100644 --- a/internal/entities/investment.go +++ b/internal/entities/investment.go @@ -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 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"` } diff --git a/internal/infrastructure/persistence/mongodb/investment.go b/internal/infrastructure/persistence/mongodb/investment.go new file mode 100644 index 0000000..108adf7 --- /dev/null +++ b/internal/infrastructure/persistence/mongodb/investment.go @@ -0,0 +1,58 @@ +package mongodb + +import ( + "context" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + + "github.com/Financial-Partner/server/internal/entities" +) + +type MongoInvestmentResporitory struct { + collection *mongo.Collection +} + +func NewInvestmentRepository(db MongoClient) *MongoInvestmentResporitory { + return &MongoInvestmentResporitory{ + collection: db.Collection("investments"), + } +} + +func (r *MongoInvestmentResporitory) 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 *MongoInvestmentResporitory) 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 *MongoInvestmentResporitory) 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 +} diff --git a/internal/infrastructure/persistence/mongodb/investment_test.go b/internal/infrastructure/persistence/mongodb/investment_test.go new file mode 100644 index 0000000..1619c55 --- /dev/null +++ b/internal/infrastructure/persistence/mongodb/investment_test.go @@ -0,0 +1,172 @@ +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: testUserID, + OpportunityID: primitive.NewObjectID().Hex(), + 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("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) + } + }) + }) +} diff --git a/internal/infrastructure/persistence/mongodb/transaction_test.go b/internal/infrastructure/persistence/mongodb/transaction_test.go index e5b1395..4f3a45a 100644 --- a/internal/infrastructure/persistence/mongodb/transaction_test.go +++ b/internal/infrastructure/persistence/mongodb/transaction_test.go @@ -27,8 +27,8 @@ func TestMongoTransactionRepository(t *testing.T) { Date: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC), Category: "Food", Type: "expense", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + 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), }, { ID: primitive.NewObjectID(), @@ -38,8 +38,8 @@ func TestMongoTransactionRepository(t *testing.T) { Date: time.Date(2023, time.January, 2, 0, 0, 0, 0, time.UTC), Category: "Housing", Type: "expense", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), + CreatedAt: time.Date(2023, time.January, 30, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2023, time.January, 30, 0, 0, 0, 0, time.UTC), }, } @@ -68,14 +68,7 @@ func TestMongoTransactionRepository(t *testing.T) { // 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) + assert.Equal(t, testTransactions[i], transaction) } }) mt.Run("not found", func(mt *mtest.T) { @@ -104,13 +97,7 @@ func TestMongoTransactionRepository(t *testing.T) { 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) + assert.Equal(t, testTransactions[0], *result) }) mt.Run("error", func(mt *mtest.T) { mt.AddMockResponses(mtest.CreateCommandErrorResponse(mtest.CommandError{ diff --git a/internal/infrastructure/persistence/redis/investment.go b/internal/infrastructure/persistence/redis/investment.go new file mode 100644 index 0000000..9fc617c --- /dev/null +++ b/internal/infrastructure/persistence/redis/investment.go @@ -0,0 +1,68 @@ +package redis + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/Financial-Partner/server/internal/entities" +) + +const ( + investmentCacheKey = "user:%s:investments" + opportunityCacheKey = "user:%s:opportunities" + investmentCacheTTL = time.Hour * 24 +) + +type InvestmentStore struct { + cacheClient RedisClient +} + +func NewInvestmentStore(cacheClient RedisClient) *InvestmentStore { + return &InvestmentStore{cacheClient: cacheClient} +} + +func (s *InvestmentStore) SetOpportunities(ctx context.Context, userID string, opportunities []entities.Opportunity) error { + data, err := json.Marshal(opportunities) + if err != nil { + return err + } + + return s.cacheClient.Set(ctx, fmt.Sprintf(opportunityCacheKey, userID), data, investmentCacheTTL) +} + +func (s *InvestmentStore) SetInvestments(ctx context.Context, userID string, investments []entities.Investment) error { + data, err := json.Marshal(investments) + if err != nil { + return err + } + + return s.cacheClient.Set(ctx, fmt.Sprintf(investmentCacheKey, userID), data, investmentCacheTTL) +} + +func (s *InvestmentStore) DeleteInvestments(ctx context.Context, userID string) error { + return s.cacheClient.Delete(ctx, fmt.Sprintf(investmentCacheKey, userID)) +} + +func (s *InvestmentStore) DeleteOpportunities(ctx context.Context, userID string) error { + return s.cacheClient.Delete(ctx, fmt.Sprintf(opportunityCacheKey, userID)) +} + +func (s *InvestmentStore) GetOpportunities(ctx context.Context, userID string) ([]entities.Opportunity, error) { + var opportunities []entities.Opportunity + err := s.cacheClient.Get(ctx, fmt.Sprintf(opportunityCacheKey, userID), &opportunities) + if err != nil { + return nil, err + } + return opportunities, nil +} + +func (s *InvestmentStore) GetInvestments(ctx context.Context, userID string) ([]entities.Investment, error) { + var investments []entities.Investment + err := s.cacheClient.Get(ctx, fmt.Sprintf(investmentCacheKey, userID), &investments) + if err != nil { + return nil, err + } + return investments, nil +} diff --git a/internal/infrastructure/persistence/redis/investment_test.go b/internal/infrastructure/persistence/redis/investment_test.go new file mode 100644 index 0000000..83aaa75 --- /dev/null +++ b/internal/infrastructure/persistence/redis/investment_test.go @@ -0,0 +1,161 @@ +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" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +func TestInvestmentStore(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("SetOpportunitiesSuccess", func(t *testing.T) { + mockRedisClient := redis.NewMockRedisClient(ctrl) + investmentStore := redis.NewInvestmentStore(mockRedisClient) + + // Mock data + userID := primitive.NewObjectID().Hex() + opportunities := []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), + }, + } + + mockRedisClient.EXPECT().Set(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + err := investmentStore.SetOpportunities(context.Background(), userID, opportunities) + require.NoError(t, err) + }) + + t.Run("SetInvestmentsSuccess", func(t *testing.T) { + mockRedisClient := redis.NewMockRedisClient(ctrl) + investmentStore := redis.NewInvestmentStore(mockRedisClient) + + // Mock data + userID := primitive.NewObjectID().Hex() + investments := []entities.Investment{ + { + ID: primitive.NewObjectID(), + UserID: primitive.NewObjectID().Hex(), + OpportunityID: primitive.NewObjectID().Hex(), + 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), + }, + } + + mockRedisClient.EXPECT().Set(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + + err := investmentStore.SetInvestments(context.Background(), userID, investments) + require.NoError(t, err) + }) + + t.Run("DeleteInvestmentsSuccess", func(t *testing.T) { + mockRedisClient := redis.NewMockRedisClient(ctrl) + investmentStore := redis.NewInvestmentStore(mockRedisClient) + + userID := primitive.NewObjectID().Hex() + mockRedisClient.EXPECT().Delete(gomock.Any(), fmt.Sprintf("user:%s:investments", userID)).Return(nil) + + err := investmentStore.DeleteInvestments(context.Background(), userID) + require.NoError(t, err) + }) + + t.Run("DeleteOpportunitiesSuccess", func(t *testing.T) { + mockRedisClient := redis.NewMockRedisClient(ctrl) + investmentStore := redis.NewInvestmentStore(mockRedisClient) + + userID := primitive.NewObjectID().Hex() + mockRedisClient.EXPECT().Delete(gomock.Any(), fmt.Sprintf("user:%s:opportunities", userID)).Return(nil) + + err := investmentStore.DeleteOpportunities(context.Background(), userID) + require.NoError(t, err) + }) + + t.Run("GetInvestmentsSuccess", func(t *testing.T) { + mockRedisClient := redis.NewMockRedisClient(ctrl) + investmentStore := redis.NewInvestmentStore(mockRedisClient) + + // Mock data + userID := primitive.NewObjectID().Hex() + + mockInvestments := []entities.Investment{ + { + ID: primitive.NewObjectID(), + UserID: primitive.NewObjectID().Hex(), + OpportunityID: primitive.NewObjectID().Hex(), + 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), + }, + } + + // Serialize mockInvestments to JSON + mockData, _ := json.Marshal(mockInvestments) + + // Mock the Get method to return the serialized JSON data + mockRedisClient.EXPECT().Get(gomock.Any(), fmt.Sprintf("user:%s:investments", userID), gomock.Any()).DoAndReturn( + func(_ context.Context, _ string, dest interface{}) error { + return json.Unmarshal(mockData, dest) + }, + ) + investments, err := investmentStore.GetInvestments(context.Background(), userID) + require.NoError(t, err) + assert.NotNil(t, investments) + }) + + t.Run("GetOpportunitiesSuccess", func(t *testing.T) { + mockRedisClient := redis.NewMockRedisClient(ctrl) + investmentStore := redis.NewInvestmentStore(mockRedisClient) + + // Mock data + userID := primitive.NewObjectID().Hex() + + mockOpportunities := []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), + }, + } + + // Serialize mockOpportunities to JSON + mockData, _ := json.Marshal(mockOpportunities) + + // Mock the Get method to return the serialized JSON data + mockRedisClient.EXPECT().Get(gomock.Any(), fmt.Sprintf("user:%s:opportunities", userID), gomock.Any()).DoAndReturn( + func(_ context.Context, _ string, dest interface{}) error { + return json.Unmarshal(mockData, dest) + }, + ) + opportunities, err := investmentStore.GetOpportunities(context.Background(), userID) + require.NoError(t, err) + assert.NotNil(t, opportunities) + }) +} diff --git a/internal/interfaces/http/dto/investment.go b/internal/interfaces/http/dto/investment.go index a83c0bc..2ce19cc 100644 --- a/internal/interfaces/http/dto/investment.go +++ b/internal/interfaces/http/dto/investment.go @@ -1,7 +1,6 @@ package dto type OpportunityResponse struct { - ID string `json:"id" example:"60d6ec33f777b123e4567890"` Title string `json:"title" example:"Investment in stock market"` Description string `json:"description" example:"Investment in stock market is a good way to make money"` Tags []string `json:"tags" example:"stock, market"` @@ -14,7 +13,6 @@ type OpportunityResponse struct { } type InvestmentResponse struct { - ID string `json:"id" example:"60d6ec33f777b123e4567890"` OpportunityID string `json:"opportunity_id" example:"60d6ec33f777b123e4567890"` UserID string `json:"user_id" example:"60d6ec33f777b123e4567890"` Amount int64 `json:"amount" example:"1000"` diff --git a/internal/interfaces/http/investment.go b/internal/interfaces/http/investment.go index 3e7f757..2ed8136 100644 --- a/internal/interfaces/http/investment.go +++ b/internal/interfaces/http/investment.go @@ -49,7 +49,6 @@ func (h *Handler) GetOpportunities(w http.ResponseWriter, r *http.Request) { var opportunitiesResponses []dto.OpportunityResponse for _, opportunity := range opportunities { opportunitiesResponses = append(opportunitiesResponses, dto.OpportunityResponse{ - ID: opportunity.ID, Title: opportunity.Title, Description: opportunity.Description, Tags: opportunity.Tags, @@ -104,7 +103,6 @@ func (h *Handler) CreateUserInvestment(w http.ResponseWriter, r *http.Request) { } resp := dto.InvestmentResponse{ - ID: investment.ID, OpportunityID: investment.OpportunityID, UserID: investment.UserID, Amount: investment.Amount, @@ -143,10 +141,10 @@ func (h *Handler) GetUserInvestments(w http.ResponseWriter, r *http.Request) { var investmentsResponse []dto.InvestmentResponse for _, investment := range investments { investmentsResponse = append(investmentsResponse, dto.InvestmentResponse{ - ID: investment.ID, - Amount: investment.Amount, - CreatedAt: investment.CreatedAt.Format(time.RFC3339), - UpdatedAt: investment.UpdatedAt.Format(time.RFC3339), + OpportunityID: investment.OpportunityID, + Amount: investment.Amount, + CreatedAt: investment.CreatedAt.Format(time.RFC3339), + UpdatedAt: investment.UpdatedAt.Format(time.RFC3339), }) } diff --git a/internal/interfaces/http/investment_test.go b/internal/interfaces/http/investment_test.go index 8bff8b7..509d3b2 100644 --- a/internal/interfaces/http/investment_test.go +++ b/internal/interfaces/http/investment_test.go @@ -76,7 +76,7 @@ func TestGetOpportunities(t *testing.T) { userEmail := "test@example.com" opportunities := []entities.Opportunity{ { - ID: "investment_123456", + ID: primitive.NewObjectID(), Title: "Test investments", Description: "Test description", Tags: []string{"test", "investments"}, @@ -151,7 +151,7 @@ func TestCreateUserInvestment(t *testing.T) { h, _ := newTestHandler(t) req := dto.CreateUserInvestmentRequest{ - OpportunityID: "opportunity_123456", + OpportunityID: primitive.NewObjectID().Hex(), Amount: 1000, } body, _ := json.Marshal(req) @@ -181,7 +181,7 @@ func TestCreateUserInvestment(t *testing.T) { Return(nil, errors.New("service error")) req := dto.CreateUserInvestmentRequest{ - OpportunityID: "opportunity_123456", + OpportunityID: primitive.NewObjectID().Hex(), Amount: 1000, } body, _ := json.Marshal(req) @@ -210,9 +210,9 @@ func TestCreateUserInvestment(t *testing.T) { now := time.Now() investment := &entities.Investment{ - ID: "investment_123456", + ID: primitive.NewObjectID(), UserID: userID, - OpportunityID: "opportunity_123", + OpportunityID: primitive.NewObjectID().Hex(), Amount: 1000, CreatedAt: now, UpdatedAt: now, @@ -223,7 +223,7 @@ func TestCreateUserInvestment(t *testing.T) { Return(investment, nil) req := dto.CreateUserInvestmentRequest{ - OpportunityID: "opportunity_123", + OpportunityID: primitive.NewObjectID().Hex(), Amount: 1000, } @@ -305,10 +305,11 @@ func TestGetUserInvestments(t *testing.T) { userEmail := "test@example.com" investments := []entities.Investment{ { - ID: "investment_123456", - Amount: 1000, - CreatedAt: now.AddDate(0, -1, 0), - UpdatedAt: now, + ID: primitive.NewObjectID(), + OpportunityID: primitive.NewObjectID().Hex(), + Amount: 1000, + CreatedAt: now.AddDate(0, -1, 0), + UpdatedAt: now, }, } @@ -331,7 +332,7 @@ func TestGetUserInvestments(t *testing.T) { assert.NoError(t, err) // Compare the response with the expected data assert.Len(t, response.Investments, 1) - assert.Equal(t, investments[0].ID, response.Investments[0].ID) + assert.Equal(t, investments[0].OpportunityID, response.Investments[0].OpportunityID) assert.Equal(t, investments[0].Amount, response.Investments[0].Amount) assert.Equal(t, investments[0].CreatedAt.Format(time.RFC3339), response.Investments[0].CreatedAt) assert.Equal(t, investments[0].UpdatedAt.Format(time.RFC3339), response.Investments[0].UpdatedAt) diff --git a/internal/module/investment/domain/interfaces.go b/internal/module/investment/domain/interfaces.go new file mode 100644 index 0000000..c97bc18 --- /dev/null +++ b/internal/module/investment/domain/interfaces.go @@ -0,0 +1,16 @@ +package investment_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=investment_domain + +type InvestmentService interface { + GetOpportunities(ctx context.Context, userID string) ([]entities.Opportunity, error) + CreateUserInvestment(ctx context.Context, userID string, req *dto.CreateUserInvestmentRequest) (*entities.Investment, error) + GetUserInvestments(ctx context.Context, userID string) ([]entities.Investment, error) +} diff --git a/internal/module/investment/domain/interfaces_mock.go b/internal/module/investment/domain/interfaces_mock.go new file mode 100644 index 0000000..05ac4d5 --- /dev/null +++ b/internal/module/investment/domain/interfaces_mock.go @@ -0,0 +1,88 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: interfaces.go +// +// Generated by this command: +// +// mockgen -source=interfaces.go -destination=interfaces_mock.go -package=investment_domain +// + +// Package investment_domain is a generated GoMock package. +package investment_domain + +import ( + context "context" + reflect "reflect" + + entities "github.com/Financial-Partner/server/internal/entities" + dto "github.com/Financial-Partner/server/internal/interfaces/http/dto" + gomock "go.uber.org/mock/gomock" +) + +// MockInvestmentService is a mock of InvestmentService interface. +type MockInvestmentService struct { + ctrl *gomock.Controller + recorder *MockInvestmentServiceMockRecorder + isgomock struct{} +} + +// MockInvestmentServiceMockRecorder is the mock recorder for MockInvestmentService. +type MockInvestmentServiceMockRecorder struct { + mock *MockInvestmentService +} + +// NewMockInvestmentService creates a new mock instance. +func NewMockInvestmentService(ctrl *gomock.Controller) *MockInvestmentService { + mock := &MockInvestmentService{ctrl: ctrl} + mock.recorder = &MockInvestmentServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInvestmentService) EXPECT() *MockInvestmentServiceMockRecorder { + return m.recorder +} + +// CreateUserInvestment mocks base method. +func (m *MockInvestmentService) CreateUserInvestment(ctx context.Context, userID string, req *dto.CreateUserInvestmentRequest) (*entities.Investment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUserInvestment", ctx, userID, req) + ret0, _ := ret[0].(*entities.Investment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUserInvestment indicates an expected call of CreateUserInvestment. +func (mr *MockInvestmentServiceMockRecorder) CreateUserInvestment(ctx, userID, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserInvestment", reflect.TypeOf((*MockInvestmentService)(nil).CreateUserInvestment), ctx, userID, req) +} + +// GetOpportunities mocks base method. +func (m *MockInvestmentService) GetOpportunities(ctx context.Context, userID string) ([]entities.Opportunity, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOpportunities", ctx, userID) + ret0, _ := ret[0].([]entities.Opportunity) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOpportunities indicates an expected call of GetOpportunities. +func (mr *MockInvestmentServiceMockRecorder) GetOpportunities(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOpportunities", reflect.TypeOf((*MockInvestmentService)(nil).GetOpportunities), ctx, userID) +} + +// GetUserInvestments mocks base method. +func (m *MockInvestmentService) GetUserInvestments(ctx context.Context, userID string) ([]entities.Investment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetUserInvestments", ctx, userID) + ret0, _ := ret[0].([]entities.Investment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetUserInvestments indicates an expected call of GetUserInvestments. +func (mr *MockInvestmentServiceMockRecorder) GetUserInvestments(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInvestments", reflect.TypeOf((*MockInvestmentService)(nil).GetUserInvestments), ctx, userID) +} diff --git a/internal/module/investment/repository/repository.go b/internal/module/investment/repository/repository.go new file mode 100644 index 0000000..8b51d81 --- /dev/null +++ b/internal/module/investment/repository/repository.go @@ -0,0 +1,24 @@ +package investment_repository + +import ( + "context" + + "github.com/Financial-Partner/server/internal/entities" +) + +//go:generate mockgen -source=repository.go -destination=repository_mock.go -package=investment_repository + +type Repository interface { + CreateInvestment(ctx context.Context, entity *entities.Investment) (*entities.Investment, error) + FindOpportunitiesByUserId(ctx context.Context, userID string) ([]entities.Opportunity, error) + FindInvestmentsByUserId(ctx context.Context, userID string) ([]entities.Investment, error) +} + +type InvestmentStore interface { + GetOpportunities(ctx context.Context, userID string) ([]entities.Opportunity, error) + GetInvestments(ctx context.Context, userID string) ([]entities.Investment, error) + SetOpportunities(ctx context.Context, userID string, opportunities []entities.Opportunity) error + SetInvestments(ctx context.Context, userID string, investments []entities.Investment) error + DeleteInvestments(ctx context.Context, userID string) error + DeleteOpportunities(ctx context.Context, userID string) error +} diff --git a/internal/module/investment/repository/repository_mock.go b/internal/module/investment/repository/repository_mock.go new file mode 100644 index 0000000..11b833c --- /dev/null +++ b/internal/module/investment/repository/repository_mock.go @@ -0,0 +1,197 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: repository.go +// +// Generated by this command: +// +// mockgen -source=repository.go -destination=repository_mock.go -package=investment_repository +// + +// Package investment_repository is a generated GoMock package. +package investment_repository + +import ( + context "context" + reflect "reflect" + + entities "github.com/Financial-Partner/server/internal/entities" + gomock "go.uber.org/mock/gomock" +) + +// MockRepository is a mock of Repository interface. +type MockRepository struct { + ctrl *gomock.Controller + recorder *MockRepositoryMockRecorder + isgomock struct{} +} + +// MockRepositoryMockRecorder is the mock recorder for MockRepository. +type MockRepositoryMockRecorder struct { + mock *MockRepository +} + +// NewMockRepository creates a new mock instance. +func NewMockRepository(ctrl *gomock.Controller) *MockRepository { + mock := &MockRepository{ctrl: ctrl} + mock.recorder = &MockRepositoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { + return m.recorder +} + +// CreateInvestment mocks base method. +func (m *MockRepository) CreateInvestment(ctx context.Context, entity *entities.Investment) (*entities.Investment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateInvestment", ctx, entity) + ret0, _ := ret[0].(*entities.Investment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateInvestment indicates an expected call of CreateInvestment. +func (mr *MockRepositoryMockRecorder) CreateInvestment(ctx, entity any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateInvestment", reflect.TypeOf((*MockRepository)(nil).CreateInvestment), ctx, entity) +} + +// FindInvestmentsByUserId mocks base method. +func (m *MockRepository) FindInvestmentsByUserId(ctx context.Context, userID string) ([]entities.Investment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindInvestmentsByUserId", ctx, userID) + ret0, _ := ret[0].([]entities.Investment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindInvestmentsByUserId indicates an expected call of FindInvestmentsByUserId. +func (mr *MockRepositoryMockRecorder) FindInvestmentsByUserId(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindInvestmentsByUserId", reflect.TypeOf((*MockRepository)(nil).FindInvestmentsByUserId), ctx, userID) +} + +// FindOpportunitiesByUserId mocks base method. +func (m *MockRepository) FindOpportunitiesByUserId(ctx context.Context, userID string) ([]entities.Opportunity, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindOpportunitiesByUserId", ctx, userID) + ret0, _ := ret[0].([]entities.Opportunity) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindOpportunitiesByUserId indicates an expected call of FindOpportunitiesByUserId. +func (mr *MockRepositoryMockRecorder) FindOpportunitiesByUserId(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindOpportunitiesByUserId", reflect.TypeOf((*MockRepository)(nil).FindOpportunitiesByUserId), ctx, userID) +} + +// MockInvestmentStore is a mock of InvestmentStore interface. +type MockInvestmentStore struct { + ctrl *gomock.Controller + recorder *MockInvestmentStoreMockRecorder + isgomock struct{} +} + +// MockInvestmentStoreMockRecorder is the mock recorder for MockInvestmentStore. +type MockInvestmentStoreMockRecorder struct { + mock *MockInvestmentStore +} + +// NewMockInvestmentStore creates a new mock instance. +func NewMockInvestmentStore(ctrl *gomock.Controller) *MockInvestmentStore { + mock := &MockInvestmentStore{ctrl: ctrl} + mock.recorder = &MockInvestmentStoreMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockInvestmentStore) EXPECT() *MockInvestmentStoreMockRecorder { + return m.recorder +} + +// DeleteInvestments mocks base method. +func (m *MockInvestmentStore) DeleteInvestments(ctx context.Context, userID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteInvestments", ctx, userID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteInvestments indicates an expected call of DeleteInvestments. +func (mr *MockInvestmentStoreMockRecorder) DeleteInvestments(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteInvestments", reflect.TypeOf((*MockInvestmentStore)(nil).DeleteInvestments), ctx, userID) +} + +// DeleteOpportunities mocks base method. +func (m *MockInvestmentStore) DeleteOpportunities(ctx context.Context, userID string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteOpportunities", ctx, userID) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteOpportunities indicates an expected call of DeleteOpportunities. +func (mr *MockInvestmentStoreMockRecorder) DeleteOpportunities(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteOpportunities", reflect.TypeOf((*MockInvestmentStore)(nil).DeleteOpportunities), ctx, userID) +} + +// GetInvestments mocks base method. +func (m *MockInvestmentStore) GetInvestments(ctx context.Context, userID string) ([]entities.Investment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetInvestments", ctx, userID) + ret0, _ := ret[0].([]entities.Investment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetInvestments indicates an expected call of GetInvestments. +func (mr *MockInvestmentStoreMockRecorder) GetInvestments(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInvestments", reflect.TypeOf((*MockInvestmentStore)(nil).GetInvestments), ctx, userID) +} + +// GetOpportunities mocks base method. +func (m *MockInvestmentStore) GetOpportunities(ctx context.Context, userID string) ([]entities.Opportunity, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetOpportunities", ctx, userID) + ret0, _ := ret[0].([]entities.Opportunity) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetOpportunities indicates an expected call of GetOpportunities. +func (mr *MockInvestmentStoreMockRecorder) GetOpportunities(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOpportunities", reflect.TypeOf((*MockInvestmentStore)(nil).GetOpportunities), ctx, userID) +} + +// SetInvestments mocks base method. +func (m *MockInvestmentStore) SetInvestments(ctx context.Context, userID string, investments []entities.Investment) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetInvestments", ctx, userID, investments) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetInvestments indicates an expected call of SetInvestments. +func (mr *MockInvestmentStoreMockRecorder) SetInvestments(ctx, userID, investments any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetInvestments", reflect.TypeOf((*MockInvestmentStore)(nil).SetInvestments), ctx, userID, investments) +} + +// SetOpportunities mocks base method. +func (m *MockInvestmentStore) SetOpportunities(ctx context.Context, userID string, opportunities []entities.Opportunity) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetOpportunities", ctx, userID, opportunities) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetOpportunities indicates an expected call of SetOpportunities. +func (mr *MockInvestmentStoreMockRecorder) SetOpportunities(ctx, userID, opportunities any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetOpportunities", reflect.TypeOf((*MockInvestmentStore)(nil).SetOpportunities), ctx, userID, opportunities) +} diff --git a/internal/module/transaction/repository/repository.go b/internal/module/transaction/repository/repository.go index eb5fe52..99d18f0 100644 --- a/internal/module/transaction/repository/repository.go +++ b/internal/module/transaction/repository/repository.go @@ -6,7 +6,7 @@ import ( "github.com/Financial-Partner/server/internal/entities" ) -//go:generate mockgen -source=repository.go -destination=transaction_repository_mock.go -package=transaction_repository +//go:generate mockgen -source=repository.go -destination=repository_mock.go -package=transaction_repository type Repository interface { Create(ctx context.Context, transaction *entities.Transaction) (*entities.Transaction, error) diff --git a/internal/module/transaction/repository/transaction_repository_mock.go b/internal/module/transaction/repository/repository_mock.go similarity index 97% rename from internal/module/transaction/repository/transaction_repository_mock.go rename to internal/module/transaction/repository/repository_mock.go index 8b9bfcf..20c2ca3 100644 --- a/internal/module/transaction/repository/transaction_repository_mock.go +++ b/internal/module/transaction/repository/repository_mock.go @@ -3,7 +3,7 @@ // // Generated by this command: // -// mockgen -source=repository.go -destination=transaction_repository_mock.go -package=transaction_repository +// mockgen -source=repository.go -destination=repository_mock.go -package=transaction_repository // // Package transaction_repository is a generated GoMock package. diff --git a/swagger/docs.go b/swagger/docs.go index eb3ed0c..7f09c03 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -1235,10 +1235,6 @@ const docTemplate = `{ "type": "string", "example": "2023-01-01T00:00:00Z" }, - "id": { - "type": "string", - "example": "60d6ec33f777b123e4567890" - }, "opportunity_id": { "type": "string", "example": "60d6ec33f777b123e4567890" @@ -1329,10 +1325,6 @@ const docTemplate = `{ "type": "string", "example": "a month" }, - "id": { - "type": "string", - "example": "60d6ec33f777b123e4567890" - }, "is_increase": { "type": "boolean", "example": true diff --git a/swagger/swagger.json b/swagger/swagger.json index ce4c048..587b5f7 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1228,10 +1228,6 @@ "type": "string", "example": "2023-01-01T00:00:00Z" }, - "id": { - "type": "string", - "example": "60d6ec33f777b123e4567890" - }, "opportunity_id": { "type": "string", "example": "60d6ec33f777b123e4567890" @@ -1322,10 +1318,6 @@ "type": "string", "example": "a month" }, - "id": { - "type": "string", - "example": "60d6ec33f777b123e4567890" - }, "is_increase": { "type": "boolean", "example": true diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index e250e63..52d9c0d 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -207,9 +207,6 @@ definitions: created_at: example: "2023-01-01T00:00:00Z" type: string - id: - example: 60d6ec33f777b123e4567890 - type: string opportunity_id: example: 60d6ec33f777b123e4567890 type: string @@ -273,9 +270,6 @@ definitions: duration: example: a month type: string - id: - example: 60d6ec33f777b123e4567890 - type: string is_increase: example: true type: boolean From 7d03bd9faf0b8a6563afe2ab1247a3740f507bf1 Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Mon, 21 Apr 2025 15:16:10 +0800 Subject: [PATCH 2/4] Implement CreateOpportunity functionality and update related structures --- cmd/server/route.go | 1 + internal/entities/goal.go | 34 ++--- internal/entities/investment.go | 4 +- internal/entities/transaction.go | 2 +- .../persistence/mongodb/investment.go | 8 ++ .../persistence/mongodb/investment_test.go | 32 ++++- .../persistence/mongodb/transaction_test.go | 4 +- .../persistence/redis/investment_test.go | 8 +- .../persistence/redis/transaction_test.go | 8 +- internal/interfaces/http/dto/investment.go | 34 +++-- internal/interfaces/http/error/error.go | 1 + internal/interfaces/http/goals_test.go | 8 +- internal/interfaces/http/investment.go | 56 +++++++- internal/interfaces/http/investment_mock.go | 47 ++++--- internal/interfaces/http/investment_test.go | 101 +++++++++++++- internal/interfaces/http/transaction_test.go | 4 +- .../module/investment/domain/interfaces.go | 1 + .../investment/repository/repository.go | 1 + internal/module/investment/usecase/service.go | 4 + swagger/docs.go | 127 +++++++++++++++++- swagger/swagger.json | 127 +++++++++++++++++- swagger/swagger.yaml | 91 ++++++++++++- 22 files changed, 613 insertions(+), 90 deletions(-) diff --git a/cmd/server/route.go b/cmd/server/route.go index 4796951..e1f6b07 100644 --- a/cmd/server/route.go +++ b/cmd/server/route.go @@ -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) diff --git a/internal/entities/goal.go b/internal/entities/goal.go index 871f0c7..6222d47 100644 --- a/internal/entities/goal.go +++ b/internal/entities/goal.go @@ -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"` } diff --git a/internal/entities/investment.go b/internal/entities/investment.go index 904976c..6c9eea8 100644 --- a/internal/entities/investment.go +++ b/internal/entities/investment.go @@ -21,8 +21,8 @@ type Opportunity struct { type Investment struct { ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` - UserID string `bson:"user_id" json:"user_id"` - OpportunityID string `bson:"opportunity_id" json:"opportunity_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"` diff --git a/internal/entities/transaction.go b/internal/entities/transaction.go index 11cd19a..e0dec7d 100644 --- a/internal/entities/transaction.go +++ b/internal/entities/transaction.go @@ -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"` diff --git a/internal/infrastructure/persistence/mongodb/investment.go b/internal/infrastructure/persistence/mongodb/investment.go index 108adf7..f64f185 100644 --- a/internal/infrastructure/persistence/mongodb/investment.go +++ b/internal/infrastructure/persistence/mongodb/investment.go @@ -27,6 +27,14 @@ func (r *MongoInvestmentResporitory) CreateInvestment(ctx context.Context, entit return entity, nil } +func (r *MongoInvestmentResporitory) 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 *MongoInvestmentResporitory) FindOpportunitiesByUserId(ctx context.Context, userID string) ([]entities.Opportunity, error) { var opportunities []entities.Opportunity cursor, err := r.collection.Find(ctx, bson.M{"user_id": userID}) diff --git a/internal/infrastructure/persistence/mongodb/investment_test.go b/internal/infrastructure/persistence/mongodb/investment_test.go index 1619c55..345d319 100644 --- a/internal/infrastructure/persistence/mongodb/investment_test.go +++ b/internal/infrastructure/persistence/mongodb/investment_test.go @@ -20,8 +20,8 @@ func TestMongoInvestmentRepository(t *testing.T) { testInvestment := &entities.Investment{ ID: primitive.NewObjectID(), - UserID: testUserID, - OpportunityID: primitive.NewObjectID().Hex(), + 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), @@ -94,6 +94,34 @@ func TestMongoInvestmentRepository(t *testing.T) { }) }) + 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( diff --git a/internal/infrastructure/persistence/mongodb/transaction_test.go b/internal/infrastructure/persistence/mongodb/transaction_test.go index 4f3a45a..accfde9 100644 --- a/internal/infrastructure/persistence/mongodb/transaction_test.go +++ b/internal/infrastructure/persistence/mongodb/transaction_test.go @@ -21,7 +21,7 @@ func TestMongoTransactionRepository(t *testing.T) { testTransactions := []entities.Transaction{ { ID: primitive.NewObjectID(), - UserID: testUserID, + UserID: primitive.NewObjectID(), Amount: 100, Description: "Dinner", Date: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC), @@ -32,7 +32,7 @@ func TestMongoTransactionRepository(t *testing.T) { }, { ID: primitive.NewObjectID(), - UserID: testUserID, + UserID: primitive.NewObjectID(), Amount: 200, Description: "Rent", Date: time.Date(2023, time.January, 2, 0, 0, 0, 0, time.UTC), diff --git a/internal/infrastructure/persistence/redis/investment_test.go b/internal/infrastructure/persistence/redis/investment_test.go index 83aaa75..21217cc 100644 --- a/internal/infrastructure/persistence/redis/investment_test.go +++ b/internal/infrastructure/persistence/redis/investment_test.go @@ -55,8 +55,8 @@ func TestInvestmentStore(t *testing.T) { investments := []entities.Investment{ { ID: primitive.NewObjectID(), - UserID: primitive.NewObjectID().Hex(), - OpportunityID: primitive.NewObjectID().Hex(), + 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), @@ -101,8 +101,8 @@ func TestInvestmentStore(t *testing.T) { mockInvestments := []entities.Investment{ { ID: primitive.NewObjectID(), - UserID: primitive.NewObjectID().Hex(), - OpportunityID: primitive.NewObjectID().Hex(), + 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), diff --git a/internal/infrastructure/persistence/redis/transaction_test.go b/internal/infrastructure/persistence/redis/transaction_test.go index 430dfe6..6e9e6fe 100644 --- a/internal/infrastructure/persistence/redis/transaction_test.go +++ b/internal/infrastructure/persistence/redis/transaction_test.go @@ -30,7 +30,7 @@ func TestTransactionStore(t *testing.T) { mockTransactions := []entities.Transaction{ { ID: primitive.NewObjectID(), - UserID: userID, + UserID: primitive.NewObjectID(), Amount: 100, Description: "Groceries", Date: time.Now(), @@ -41,7 +41,7 @@ func TestTransactionStore(t *testing.T) { }, { ID: primitive.NewObjectID(), - UserID: userID, + UserID: primitive.NewObjectID(), Amount: 200, Description: "Rent", Date: time.Now(), @@ -87,7 +87,7 @@ func TestTransactionStore(t *testing.T) { mockTransactions := []entities.Transaction{ { ID: primitive.NewObjectID(), - UserID: userID, + UserID: primitive.NewObjectID(), Amount: 100, Description: "Groceries", Date: time.Now(), @@ -98,7 +98,7 @@ func TestTransactionStore(t *testing.T) { }, { ID: primitive.NewObjectID(), - UserID: userID, + UserID: primitive.NewObjectID(), Amount: 200, Description: "Rent", Date: time.Now(), diff --git a/internal/interfaces/http/dto/investment.go b/internal/interfaces/http/dto/investment.go index 2ce19cc..e7250fc 100644 --- a/internal/interfaces/http/dto/investment.go +++ b/internal/interfaces/http/dto/investment.go @@ -1,20 +1,20 @@ package dto type OpportunityResponse struct { - Title string `json:"title" example:"Investment in stock market"` - Description string `json:"description" example:"Investment in stock market is a good way to make money"` - Tags []string `json:"tags" example:"stock, market"` - IsIncrease bool `json:"is_increase" example:"true"` - Variation int64 `json:"variation" example:"20"` - Duration string `json:"duration" example:"a month"` - MinAmount int64 `json:"min_amount" example:"1000"` - CreatedAt string `json:"created_at" example:"2023-01-01T00:00:00Z"` - UpdatedAt string `json:"updated_at" example:"2023-06-01T00:00:00Z"` + OpportunityID string `json:"opportunity_id" example:"60d6ec33f777b123e4567890"` + Title string `json:"title" example:"Real Estate"` + Description string `json:"description" example:"Investment in stock market is a good way to make money"` + Tags []string `json:"tags" example:"high risk,long term"` + IsIncrease bool `json:"is_increase" example:"true"` + Variation int64 `json:"variation" example:"20"` + Duration string `json:"duration" example:"a month"` + MinAmount int64 `json:"min_amount" example:"1000"` + CreatedAt string `json:"created_at" example:"2023-01-01T00:00:00Z"` + UpdatedAt string `json:"updated_at" example:"2023-06-01T00:00:00Z"` } type InvestmentResponse struct { OpportunityID string `json:"opportunity_id" example:"60d6ec33f777b123e4567890"` - UserID string `json:"user_id" example:"60d6ec33f777b123e4567890"` Amount int64 `json:"amount" example:"1000"` CreatedAt string `json:"created_at" example:"2023-01-01T00:00:00Z"` UpdatedAt string `json:"updated_at" example:"2023-06-01T00:00:00Z"` @@ -36,3 +36,17 @@ type CreateUserInvestmentResponse struct { type GetUserInvestmentsResponse struct { Investments []InvestmentResponse `json:"investments"` } + +type CreateOpportunityRequest struct { + Title string `json:"title" example:"Real Estate" binding:"required"` + Description string `json:"description" example:"Investment in stock market is a good way to make money" binding:"required"` + Tags []string `json:"tags" example:"high risk,long term" binding:"required"` + IsIncrease bool `json:"is_increase" example:"true" binding:"required"` + Variation int64 `json:"variation" example:"20" binding:"required"` + Duration string `json:"duration" example:"a month" binding:"required"` + MinAmount int64 `json:"min_amount" example:"1000" binding:"required"` +} + +type CreateOpportunityResponse struct { + Opportunity OpportunityResponse `json:"opportunity"` +} diff --git a/internal/interfaces/http/error/error.go b/internal/interfaces/http/error/error.go index a774f54..ae99f37 100644 --- a/internal/interfaces/http/error/error.go +++ b/internal/interfaces/http/error/error.go @@ -19,6 +19,7 @@ const ( ErrFailedToUpdateGoal = "Failed to update goal" ErrFailedToGetGoal = "Failed to get goal" ErrFailedToGetOpportunities = "Failed to get investment opportunities" + ErrFailedToCreateOpportunity = "Failed to create investment opportunity" ErrFailedToCreateUserInvestment = "Failed to create an user investment" ErrFailedToGetUserInvestments = "Failed to get user investments" ErrFailedToGetTransactions = "Failed to get transactions" diff --git a/internal/interfaces/http/goals_test.go b/internal/interfaces/http/goals_test.go index 2ab5d52..6a30c0a 100644 --- a/internal/interfaces/http/goals_test.go +++ b/internal/interfaces/http/goals_test.go @@ -326,8 +326,8 @@ func TestCreateGoal(t *testing.T) { now := time.Now() goal := &entities.Goal{ - ID: "goal_123456", - UserID: userID, + ID: primitive.NewObjectID(), + UserID: primitive.NewObjectID(), TargetAmount: 10000, CurrentAmount: 0, Period: 30, @@ -424,8 +424,8 @@ func TestGetGoal(t *testing.T) { now := time.Now() goal := &entities.Goal{ - ID: "goal_123456", - UserID: userID, + ID: primitive.NewObjectID(), + UserID: primitive.NewObjectID(), TargetAmount: 10000, CurrentAmount: 5000, Period: 30, diff --git a/internal/interfaces/http/investment.go b/internal/interfaces/http/investment.go index 2ed8136..fb43a68 100644 --- a/internal/interfaces/http/investment.go +++ b/internal/interfaces/http/investment.go @@ -19,6 +19,7 @@ type InvestmentService interface { GetOpportunities(ctx context.Context, userID string) ([]entities.Opportunity, error) CreateUserInvestment(ctx context.Context, userID string, req *dto.CreateUserInvestmentRequest) (*entities.Investment, error) GetUserInvestments(ctx context.Context, userID string) ([]entities.Investment, error) + CreateOpportunity(ctx context.Context, userID string, req *dto.CreateOpportunityRequest) (*entities.Opportunity, error) } // @Summary Get investment opportunities @@ -103,8 +104,7 @@ func (h *Handler) CreateUserInvestment(w http.ResponseWriter, r *http.Request) { } resp := dto.InvestmentResponse{ - OpportunityID: investment.OpportunityID, - UserID: investment.UserID, + OpportunityID: investment.OpportunityID.Hex(), Amount: investment.Amount, CreatedAt: investment.CreatedAt.Format(time.RFC3339), UpdatedAt: investment.UpdatedAt.Format(time.RFC3339), @@ -141,7 +141,7 @@ func (h *Handler) GetUserInvestments(w http.ResponseWriter, r *http.Request) { var investmentsResponse []dto.InvestmentResponse for _, investment := range investments { investmentsResponse = append(investmentsResponse, dto.InvestmentResponse{ - OpportunityID: investment.OpportunityID, + OpportunityID: investment.OpportunityID.Hex(), Amount: investment.Amount, CreatedAt: investment.CreatedAt.Format(time.RFC3339), UpdatedAt: investment.UpdatedAt.Format(time.RFC3339), @@ -154,3 +154,53 @@ func (h *Handler) GetUserInvestments(w http.ResponseWriter, r *http.Request) { responde.WithJSON(w, r, resp, http.StatusOK) } + +// @Summary Create an investment opportunity +// @Description Create an investment opportunity +// @Tags investments +// @Accept json +// @Produce json +// @Param request body dto.CreateOpportunityRequest true "Create opportunity request" +// @Param Authorization header string true "Bearer {token}" default "Bearer " +// @Success 201 {object} dto.CreateOpportunityResponse +// @Failure 400 {object} dto.ErrorResponse +// @Failure 401 {object} dto.ErrorResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /investments [post] +func (h *Handler) CreateOpportunity(w http.ResponseWriter, r *http.Request) { + userID, ok := contextutil.GetUserID(r.Context()) + if !ok { + h.log.Warnf("failed to get user ID from context") + responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + return + } + + var req dto.CreateOpportunityRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + h.log.WithError(err).Warnf("failed to decode request body") + responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + return + } + + opportunity, err := h.investmentService.CreateOpportunity(r.Context(), userID, &req) + if err != nil { + h.log.WithError(err).Warnf("failed to create an user investment") + responde.WithError(w, r, h.log, err, httperror.ErrFailedToCreateOpportunity, http.StatusInternalServerError) + return + } + + resp := dto.OpportunityResponse{ + OpportunityID: opportunity.ID.Hex(), + Title: opportunity.Title, + Description: opportunity.Description, + Tags: opportunity.Tags, + IsIncrease: opportunity.IsIncrease, + Variation: opportunity.Variation, + Duration: opportunity.Duration, + MinAmount: opportunity.MinAmount, + CreatedAt: opportunity.CreatedAt.Format(time.RFC3339), + UpdatedAt: opportunity.UpdatedAt.Format(time.RFC3339), + } + + responde.WithJSON(w, r, resp, http.StatusOK) +} diff --git a/internal/interfaces/http/investment_mock.go b/internal/interfaces/http/investment_mock.go index 391db47..66f7ebc 100644 --- a/internal/interfaces/http/investment_mock.go +++ b/internal/interfaces/http/investment_mock.go @@ -42,6 +42,36 @@ func (m *MockInvestmentService) EXPECT() *MockInvestmentServiceMockRecorder { return m.recorder } +// CreateOpportunity mocks base method. +func (m *MockInvestmentService) CreateOpportunity(ctx context.Context, userID string, req *dto.CreateOpportunityRequest) (*entities.Opportunity, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateOpportunity", ctx, userID, req) + ret0, _ := ret[0].(*entities.Opportunity) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateOpportunity indicates an expected call of CreateOpportunity. +func (mr *MockInvestmentServiceMockRecorder) CreateOpportunity(ctx, userID, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOpportunity", reflect.TypeOf((*MockInvestmentService)(nil).CreateOpportunity), ctx, userID, req) +} + +// CreateUserInvestment mocks base method. +func (m *MockInvestmentService) CreateUserInvestment(ctx context.Context, userID string, req *dto.CreateUserInvestmentRequest) (*entities.Investment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateUserInvestment", ctx, userID, req) + ret0, _ := ret[0].(*entities.Investment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateUserInvestment indicates an expected call of CreateUserInvestment. +func (mr *MockInvestmentServiceMockRecorder) CreateUserInvestment(ctx, userID, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserInvestment", reflect.TypeOf((*MockInvestmentService)(nil).CreateUserInvestment), ctx, userID, req) +} + // GetOpportunities mocks base method. func (m *MockInvestmentService) GetOpportunities(ctx context.Context, userID string) ([]entities.Opportunity, error) { m.ctrl.T.Helper() @@ -66,23 +96,8 @@ func (m *MockInvestmentService) GetUserInvestments(ctx context.Context, userID s return ret0, ret1 } -// GetInvestments indicates an expected call of GetInvestments. +// GetUserInvestments indicates an expected call of GetUserInvestments. func (mr *MockInvestmentServiceMockRecorder) GetUserInvestments(ctx, userID any) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserInvestments", reflect.TypeOf((*MockInvestmentService)(nil).GetUserInvestments), ctx, userID) } - -// CreateUserInvestment mocks base method. -func (m *MockInvestmentService) CreateUserInvestment(ctx context.Context, userID string, req *dto.CreateUserInvestmentRequest) (*entities.Investment, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateUserInvestment", ctx, userID, req) - ret0, _ := ret[0].(*entities.Investment) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CreateUserInvestment indicates an expected call of CreateUserInvestment. -func (mr *MockInvestmentServiceMockRecorder) CreateUserInvestment(ctx, userID, req any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateUserInvestment", reflect.TypeOf((*MockInvestmentService)(nil).CreateUserInvestment), ctx, userID, req) -} diff --git a/internal/interfaces/http/investment_test.go b/internal/interfaces/http/investment_test.go index 509d3b2..4f925de 100644 --- a/internal/interfaces/http/investment_test.go +++ b/internal/interfaces/http/investment_test.go @@ -211,8 +211,8 @@ func TestCreateUserInvestment(t *testing.T) { now := time.Now() investment := &entities.Investment{ ID: primitive.NewObjectID(), - UserID: userID, - OpportunityID: primitive.NewObjectID().Hex(), + UserID: primitive.NewObjectID(), + OpportunityID: primitive.NewObjectID(), Amount: 1000, CreatedAt: now, UpdatedAt: now, @@ -306,7 +306,7 @@ func TestGetUserInvestments(t *testing.T) { investments := []entities.Investment{ { ID: primitive.NewObjectID(), - OpportunityID: primitive.NewObjectID().Hex(), + OpportunityID: primitive.NewObjectID(), Amount: 1000, CreatedAt: now.AddDate(0, -1, 0), UpdatedAt: now, @@ -332,9 +332,102 @@ func TestGetUserInvestments(t *testing.T) { assert.NoError(t, err) // Compare the response with the expected data assert.Len(t, response.Investments, 1) - assert.Equal(t, investments[0].OpportunityID, response.Investments[0].OpportunityID) + assert.Equal(t, investments[0].OpportunityID.Hex(), response.Investments[0].OpportunityID) assert.Equal(t, investments[0].Amount, response.Investments[0].Amount) assert.Equal(t, investments[0].CreatedAt.Format(time.RFC3339), response.Investments[0].CreatedAt) assert.Equal(t, investments[0].UpdatedAt.Format(time.RFC3339), response.Investments[0].UpdatedAt) }) } + +func TestCreateOpportunity(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("Unauthorized request", func(t *testing.T) { + h, _ := newTestHandler(t) + + req := dto.CreateOpportunityRequest{ + Title: "Test investments", + Description: "Test description", + Tags: []string{"test", "investments"}, + IsIncrease: true, + Variation: 30, + Duration: "a month", + MinAmount: 1000, + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/investments", bytes.NewBuffer(body)) + + h.CreateOpportunity(w, r) + + assert.Equal(t, http.StatusUnauthorized, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var errorResp dto.ErrorResponse + err := json.NewDecoder(w.Body).Decode(&errorResp) + assert.NoError(t, err) + assert.Equal(t, http.StatusUnauthorized, errorResp.Code) + assert.Equal(t, httperror.ErrUnauthorized, errorResp.Message) + }) + + t.Run("Invalid request format", func(t *testing.T) { + h, _ := newTestHandler(t) + + invalidBody := bytes.NewBufferString(`{invalid json`) + userID := primitive.NewObjectID().Hex() + ctx := context.WithValue(context.Background(), contextutil.UserEmailKey, "test@example.com") + ctx = context.WithValue(ctx, contextutil.UserIDKey, userID) + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/investments", invalidBody) + r = r.WithContext(ctx) + + h.CreateOpportunity(w, r) + + assert.Equal(t, http.StatusBadRequest, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var errorResp dto.ErrorResponse + err := json.NewDecoder(w.Body).Decode(&errorResp) + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, errorResp.Code) + assert.Equal(t, httperror.ErrInvalidRequest, errorResp.Message) + }) + + t.Run("Service error", func(t *testing.T) { + h, mockServices := newTestHandler(t) + + userID := primitive.NewObjectID().Hex() + userEmail := "test@example.com" + + mockServices.InvestmentService.EXPECT(). + CreateOpportunity(gomock.Any(), userID, gomock.Any()). + Return(nil, errors.New("service error")) + + req := dto.CreateOpportunityRequest{ + Title: "Test investments", + Description: "Test description", + Tags: []string{"test", "investments"}, + IsIncrease: true, + Variation: 30, + Duration: "a month", + MinAmount: 1000, + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/investments", bytes.NewBuffer(body)) + ctx := newContext(userID, userEmail) + r = r.WithContext(ctx) + + h.CreateOpportunity(w, r) + + assert.Equal(t, http.StatusInternalServerError, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var errorResp dto.ErrorResponse + err := json.NewDecoder(w.Body).Decode(&errorResp) + assert.NoError(t, err) + assert.Equal(t, http.StatusInternalServerError, errorResp.Code) + assert.Equal(t, httperror.ErrFailedToCreateOpportunity, errorResp.Message) + }) +} diff --git a/internal/interfaces/http/transaction_test.go b/internal/interfaces/http/transaction_test.go index ab0680c..57d8ae9 100644 --- a/internal/interfaces/http/transaction_test.go +++ b/internal/interfaces/http/transaction_test.go @@ -122,7 +122,7 @@ func TestCreateTransaction(t *testing.T) { Date: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC), Category: "Food", Type: "expense", - UserID: userID, + UserID: primitive.NewObjectID(), CreatedAt: now, UpdatedAt: now, } @@ -227,7 +227,7 @@ func TestGetTransactions(t *testing.T) { Date: time.Date(2023, time.January, 1, 0, 0, 0, 0, time.UTC), Category: "Food", Type: "expense", - UserID: userID, + UserID: primitive.NewObjectID(), CreatedAt: now, UpdatedAt: now, }, diff --git a/internal/module/investment/domain/interfaces.go b/internal/module/investment/domain/interfaces.go index c97bc18..20910f0 100644 --- a/internal/module/investment/domain/interfaces.go +++ b/internal/module/investment/domain/interfaces.go @@ -13,4 +13,5 @@ type InvestmentService interface { GetOpportunities(ctx context.Context, userID string) ([]entities.Opportunity, error) CreateUserInvestment(ctx context.Context, userID string, req *dto.CreateUserInvestmentRequest) (*entities.Investment, error) GetUserInvestments(ctx context.Context, userID string) ([]entities.Investment, error) + CreateOpportunity(ctx context.Context, userID string, req *dto.CreateOpportunityRequest) (*entities.Opportunity, error) } diff --git a/internal/module/investment/repository/repository.go b/internal/module/investment/repository/repository.go index 8b51d81..b938fa8 100644 --- a/internal/module/investment/repository/repository.go +++ b/internal/module/investment/repository/repository.go @@ -10,6 +10,7 @@ import ( type Repository interface { CreateInvestment(ctx context.Context, entity *entities.Investment) (*entities.Investment, error) + CreateOpportunity(ctx context.Context, entity *entities.Opportunity) (*entities.Opportunity, error) FindOpportunitiesByUserId(ctx context.Context, userID string) ([]entities.Opportunity, error) FindInvestmentsByUserId(ctx context.Context, userID string) ([]entities.Investment, error) } diff --git a/internal/module/investment/usecase/service.go b/internal/module/investment/usecase/service.go index 8fc0362..fb4b9ed 100644 --- a/internal/module/investment/usecase/service.go +++ b/internal/module/investment/usecase/service.go @@ -25,3 +25,7 @@ func (s *Service) CreateUserInvestment(ctx context.Context, userID string, req * func (s *Service) GetUserInvestments(ctx context.Context, userID string) ([]entities.Investment, error) { return nil, nil } + +func (s *Service) CreateOpportunity(ctx context.Context, userID string, req *dto.CreateOpportunityRequest) (*entities.Opportunity, error) { + return nil, nil +} diff --git a/swagger/docs.go b/swagger/docs.go index 7f09c03..09bd9f4 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -502,6 +502,63 @@ const docTemplate = `{ } } } + }, + "post": { + "description": "Create an investment opportunity", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "investments" + ], + "summary": "Create an investment opportunity", + "parameters": [ + { + "description": "Create opportunity request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateOpportunityRequest" + } + }, + { + "type": "string", + "description": "Bearer {token}", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dto.CreateOpportunityResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } } }, "/reports/analysis": { @@ -976,6 +1033,62 @@ const docTemplate = `{ } } }, + "dto.CreateOpportunityRequest": { + "type": "object", + "required": [ + "description", + "duration", + "is_increase", + "min_amount", + "tags", + "title", + "variation" + ], + "properties": { + "description": { + "type": "string", + "example": "Investment in stock market is a good way to make money" + }, + "duration": { + "type": "string", + "example": "a month" + }, + "is_increase": { + "type": "boolean", + "example": true + }, + "min_amount": { + "type": "integer", + "example": 1000 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "high risk", + "long term" + ] + }, + "title": { + "type": "string", + "example": "Real Estate" + }, + "variation": { + "type": "integer", + "example": 20 + } + } + }, + "dto.CreateOpportunityResponse": { + "type": "object", + "properties": { + "opportunity": { + "$ref": "#/definitions/dto.OpportunityResponse" + } + } + }, "dto.CreateTransactionRequest": { "type": "object", "required": [ @@ -1242,10 +1355,6 @@ const docTemplate = `{ "updated_at": { "type": "string", "example": "2023-06-01T00:00:00Z" - }, - "user_id": { - "type": "string", - "example": "60d6ec33f777b123e4567890" } } }, @@ -1333,19 +1442,23 @@ const docTemplate = `{ "type": "integer", "example": 1000 }, + "opportunity_id": { + "type": "string", + "example": "60d6ec33f777b123e4567890" + }, "tags": { "type": "array", "items": { "type": "string" }, "example": [ - "stock", - " market" + "high risk", + "long term" ] }, "title": { "type": "string", - "example": "Investment in stock market" + "example": "Real Estate" }, "updated_at": { "type": "string", diff --git a/swagger/swagger.json b/swagger/swagger.json index 587b5f7..17288a3 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -495,6 +495,63 @@ } } } + }, + "post": { + "description": "Create an investment opportunity", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "investments" + ], + "summary": "Create an investment opportunity", + "parameters": [ + { + "description": "Create opportunity request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CreateOpportunityRequest" + } + }, + { + "type": "string", + "description": "Bearer {token}", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/dto.CreateOpportunityResponse" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } } }, "/reports/analysis": { @@ -969,6 +1026,62 @@ } } }, + "dto.CreateOpportunityRequest": { + "type": "object", + "required": [ + "description", + "duration", + "is_increase", + "min_amount", + "tags", + "title", + "variation" + ], + "properties": { + "description": { + "type": "string", + "example": "Investment in stock market is a good way to make money" + }, + "duration": { + "type": "string", + "example": "a month" + }, + "is_increase": { + "type": "boolean", + "example": true + }, + "min_amount": { + "type": "integer", + "example": 1000 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "high risk", + "long term" + ] + }, + "title": { + "type": "string", + "example": "Real Estate" + }, + "variation": { + "type": "integer", + "example": 20 + } + } + }, + "dto.CreateOpportunityResponse": { + "type": "object", + "properties": { + "opportunity": { + "$ref": "#/definitions/dto.OpportunityResponse" + } + } + }, "dto.CreateTransactionRequest": { "type": "object", "required": [ @@ -1235,10 +1348,6 @@ "updated_at": { "type": "string", "example": "2023-06-01T00:00:00Z" - }, - "user_id": { - "type": "string", - "example": "60d6ec33f777b123e4567890" } } }, @@ -1326,19 +1435,23 @@ "type": "integer", "example": 1000 }, + "opportunity_id": { + "type": "string", + "example": "60d6ec33f777b123e4567890" + }, "tags": { "type": "array", "items": { "type": "string" }, "example": [ - "stock", - " market" + "high risk", + "long term" ] }, "title": { "type": "string", - "example": "Investment in stock market" + "example": "Real Estate" }, "updated_at": { "type": "string", diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index 52d9c0d..5a6cf45 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -24,6 +24,47 @@ definitions: - period - target_amount type: object + dto.CreateOpportunityRequest: + properties: + description: + example: Investment in stock market is a good way to make money + type: string + duration: + example: a month + type: string + is_increase: + example: true + type: boolean + min_amount: + example: 1000 + type: integer + tags: + example: + - high risk + - long term + items: + type: string + type: array + title: + example: Real Estate + type: string + variation: + example: 20 + type: integer + required: + - description + - duration + - is_increase + - min_amount + - tags + - title + - variation + type: object + dto.CreateOpportunityResponse: + properties: + opportunity: + $ref: '#/definitions/dto.OpportunityResponse' + type: object dto.CreateTransactionRequest: properties: amount: @@ -213,9 +254,6 @@ definitions: updated_at: example: "2023-06-01T00:00:00Z" type: string - user_id: - example: 60d6ec33f777b123e4567890 - type: string type: object dto.LoginRequest: properties: @@ -276,15 +314,18 @@ definitions: min_amount: example: 1000 type: integer + opportunity_id: + example: 60d6ec33f777b123e4567890 + type: string tags: example: - - stock - - ' market' + - high risk + - long term items: type: string type: array title: - example: Investment in stock market + example: Real Estate type: string updated_at: example: "2023-06-01T00:00:00Z" @@ -787,6 +828,44 @@ paths: summary: Get investment opportunities tags: - investments + post: + consumes: + - application/json + description: Create an investment opportunity + parameters: + - description: Create opportunity request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.CreateOpportunityRequest' + - description: Bearer {token} + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/dto.CreateOpportunityResponse' + "400": + description: Bad Request + schema: + $ref: '#/definitions/dto.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/dto.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/dto.ErrorResponse' + summary: Create an investment opportunity + tags: + - investments /reports/analysis: get: consumes: From 1d0466266cdd978aa95227179f7851713e6169ca Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Mon, 21 Apr 2025 16:14:35 +0800 Subject: [PATCH 3/4] Rename package 'responde' to 'respond' for consistency across HTTP response handling --- internal/interfaces/http/auth.go | 20 ++++++------- internal/interfaces/http/gacha.go | 16 +++++----- internal/interfaces/http/goals.go | 30 +++++++++---------- internal/interfaces/http/investment.go | 30 +++++++++---------- internal/interfaces/http/respond/respond.go | 2 +- .../interfaces/http/respond/respond_test.go | 2 +- internal/interfaces/http/transaction.go | 16 +++++----- internal/interfaces/http/user.go | 16 +++++----- 8 files changed, 66 insertions(+), 66 deletions(-) diff --git a/internal/interfaces/http/auth.go b/internal/interfaces/http/auth.go index 481e5ab..a664903 100644 --- a/internal/interfaces/http/auth.go +++ b/internal/interfaces/http/auth.go @@ -9,7 +9,7 @@ import ( "github.com/Financial-Partner/server/internal/entities" "github.com/Financial-Partner/server/internal/interfaces/http/dto" httperror "github.com/Financial-Partner/server/internal/interfaces/http/error" - responde "github.com/Financial-Partner/server/internal/interfaces/http/respond" + respond "github.com/Financial-Partner/server/internal/interfaces/http/respond" ) //go:generate mockgen -source=auth.go -destination=auth_mock.go -package=handler @@ -36,14 +36,14 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { var req dto.LoginRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("Invalid request format") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } accessToken, refreshToken, expiresIn, userInfo, err := h.authService.LoginWithFirebase(r.Context(), req.FirebaseToken) if err != nil { h.log.WithError(err).Errorf("Login failed") - responde.WithError(w, r, h.log, err, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, err, httperror.ErrUnauthorized, http.StatusUnauthorized) return } @@ -61,7 +61,7 @@ func (h *Handler) Login(w http.ResponseWriter, r *http.Request) { }, } - responde.WithJSON(w, r, response, http.StatusOK) + respond.WithJSON(w, r, response, http.StatusOK) } // RefreshToken Refresh Access Token @@ -80,14 +80,14 @@ func (h *Handler) RefreshToken(w http.ResponseWriter, r *http.Request) { var req dto.RefreshTokenRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("Invalid request format") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } newAccessToken, newRefreshToken, expiresIn, err := h.authService.RefreshToken(r.Context(), req.RefreshToken) if err != nil { h.log.WithError(err).Errorf("Token refresh failed") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRefreshToken, http.StatusUnauthorized) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRefreshToken, http.StatusUnauthorized) return } @@ -98,7 +98,7 @@ func (h *Handler) RefreshToken(w http.ResponseWriter, r *http.Request) { TokenType: "Bearer", } - responde.WithJSON(w, r, response, http.StatusOK) + respond.WithJSON(w, r, response, http.StatusOK) } // Logout Logout @@ -116,14 +116,14 @@ func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { var req dto.LogoutRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("Invalid request format") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } err := h.authService.Logout(r.Context(), req.RefreshToken) if err != nil { h.log.WithError(err).Errorf("Logout failed") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToLogout, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToLogout, http.StatusInternalServerError) return } @@ -132,5 +132,5 @@ func (h *Handler) Logout(w http.ResponseWriter, r *http.Request) { Message: "Logout successfully", } - responde.WithJSON(w, r, response, http.StatusOK) + respond.WithJSON(w, r, response, http.StatusOK) } diff --git a/internal/interfaces/http/gacha.go b/internal/interfaces/http/gacha.go index f62d732..df8ff3b 100644 --- a/internal/interfaces/http/gacha.go +++ b/internal/interfaces/http/gacha.go @@ -9,7 +9,7 @@ import ( "github.com/Financial-Partner/server/internal/entities" "github.com/Financial-Partner/server/internal/interfaces/http/dto" httperror "github.com/Financial-Partner/server/internal/interfaces/http/error" - responde "github.com/Financial-Partner/server/internal/interfaces/http/respond" + respond "github.com/Financial-Partner/server/internal/interfaces/http/respond" ) //go:generate mockgen -source=gacha.go -destination=gacha_mock.go -package=handler @@ -34,21 +34,21 @@ func (h *Handler) DrawGacha(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } var req dto.DrawGachaRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("failed to decode request body") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } gacha, err := h.gachaService.DrawGacha(r.Context(), userID, &req) if err != nil { h.log.WithError(err).Warnf("failed to draw a gacha") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToDrawGacha, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToDrawGacha, http.StatusInternalServerError) return } @@ -57,7 +57,7 @@ func (h *Handler) DrawGacha(w http.ResponseWriter, r *http.Request) { ImgSrc: gacha.ImgSrc, } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } // @Summary Get 9 gacha images for preview @@ -74,14 +74,14 @@ func (h *Handler) PreviewGachas(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } gachas, err := h.gachaService.PreviewGachas(r.Context(), userID) if err != nil { h.log.WithError(err).Warnf("failed to get preview gacha images") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToPreviewGachas, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToPreviewGachas, http.StatusInternalServerError) return } @@ -96,5 +96,5 @@ func (h *Handler) PreviewGachas(w http.ResponseWriter, r *http.Request) { }) } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } diff --git a/internal/interfaces/http/goals.go b/internal/interfaces/http/goals.go index 74b1df3..cb45648 100644 --- a/internal/interfaces/http/goals.go +++ b/internal/interfaces/http/goals.go @@ -10,7 +10,7 @@ import ( "github.com/Financial-Partner/server/internal/entities" "github.com/Financial-Partner/server/internal/interfaces/http/dto" httperror "github.com/Financial-Partner/server/internal/interfaces/http/error" - responde "github.com/Financial-Partner/server/internal/interfaces/http/respond" + respond "github.com/Financial-Partner/server/internal/interfaces/http/respond" ) //go:generate mockgen -source=goals.go -destination=goals_mock.go -package=handler @@ -37,21 +37,21 @@ func (h *Handler) GetGoalSuggestion(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } var req dto.GoalSuggestionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("failed to decode request body") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } suggestion, err := h.goalService.GetGoalSuggestion(r.Context(), userID, &req) if err != nil { h.log.WithError(err).Warnf("failed to get goal suggestion") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetGoalSuggestion, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetGoalSuggestion, http.StatusInternalServerError) return } @@ -61,7 +61,7 @@ func (h *Handler) GetGoalSuggestion(w http.ResponseWriter, r *http.Request) { Message: suggestion.Message, } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } // @Summary Calculate and return suggested saving goals based on user's expense data @@ -78,14 +78,14 @@ func (h *Handler) GetAutoGoalSuggestion(w http.ResponseWriter, r *http.Request) userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } suggestion, err := h.goalService.GetAutoGoalSuggestion(r.Context(), userID) if err != nil { h.log.WithError(err).Warnf("failed to get auto goal suggestion") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetGoalSuggestion, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetGoalSuggestion, http.StatusInternalServerError) return } @@ -95,7 +95,7 @@ func (h *Handler) GetAutoGoalSuggestion(w http.ResponseWriter, r *http.Request) Message: suggestion.Message, } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } // @Summary Create user's saving goal @@ -114,21 +114,21 @@ func (h *Handler) CreateGoal(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } var req dto.CreateGoalRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("failed to decode request body") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } goal, err := h.goalService.CreateGoal(r.Context(), userID, &req) if err != nil { h.log.WithError(err).Warnf("failed to create or update goal") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToCreateGoal, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToCreateGoal, http.StatusInternalServerError) return } @@ -141,7 +141,7 @@ func (h *Handler) CreateGoal(w http.ResponseWriter, r *http.Request) { UpdatedAt: goal.UpdatedAt.Format(time.RFC3339), } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } // @Summary Get current saving goal @@ -158,14 +158,14 @@ func (h *Handler) GetGoal(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } goal, err := h.goalService.GetGoal(r.Context(), userID) if err != nil { h.log.WithError(err).Warnf("failed to get goal") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetGoal, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetGoal, http.StatusInternalServerError) return } @@ -180,5 +180,5 @@ func (h *Handler) GetGoal(w http.ResponseWriter, r *http.Request) { }, } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } diff --git a/internal/interfaces/http/investment.go b/internal/interfaces/http/investment.go index fb43a68..3f93ccc 100644 --- a/internal/interfaces/http/investment.go +++ b/internal/interfaces/http/investment.go @@ -10,7 +10,7 @@ import ( "github.com/Financial-Partner/server/internal/entities" "github.com/Financial-Partner/server/internal/interfaces/http/dto" httperror "github.com/Financial-Partner/server/internal/interfaces/http/error" - responde "github.com/Financial-Partner/server/internal/interfaces/http/respond" + respond "github.com/Financial-Partner/server/internal/interfaces/http/respond" ) //go:generate mockgen -source=investment.go -destination=investment_mock.go -package=handler @@ -36,14 +36,14 @@ func (h *Handler) GetOpportunities(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } opportunities, err := h.investmentService.GetOpportunities(r.Context(), userID) if err != nil { h.log.WithError(err).Warnf("failed to get investment opportunities") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetOpportunities, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetOpportunities, http.StatusInternalServerError) return } @@ -66,7 +66,7 @@ func (h *Handler) GetOpportunities(w http.ResponseWriter, r *http.Request) { Opportunities: opportunitiesResponses, } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } // @Summary Create user investment @@ -85,21 +85,21 @@ func (h *Handler) CreateUserInvestment(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } var req dto.CreateUserInvestmentRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("failed to decode request body") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } investment, err := h.investmentService.CreateUserInvestment(r.Context(), userID, &req) if err != nil { h.log.WithError(err).Warnf("failed to create an user investment") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToCreateUserInvestment, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToCreateUserInvestment, http.StatusInternalServerError) return } @@ -110,7 +110,7 @@ func (h *Handler) CreateUserInvestment(w http.ResponseWriter, r *http.Request) { UpdatedAt: investment.UpdatedAt.Format(time.RFC3339), } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } // @Summary Get user investments @@ -127,14 +127,14 @@ func (h *Handler) GetUserInvestments(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } investments, err := h.investmentService.GetUserInvestments(r.Context(), userID) if err != nil { h.log.WithError(err).Warnf("failed to get user investments") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetUserInvestments, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetUserInvestments, http.StatusInternalServerError) return } @@ -152,7 +152,7 @@ func (h *Handler) GetUserInvestments(w http.ResponseWriter, r *http.Request) { Investments: investmentsResponse, } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } // @Summary Create an investment opportunity @@ -171,21 +171,21 @@ func (h *Handler) CreateOpportunity(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } var req dto.CreateOpportunityRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("failed to decode request body") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } opportunity, err := h.investmentService.CreateOpportunity(r.Context(), userID, &req) if err != nil { h.log.WithError(err).Warnf("failed to create an user investment") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToCreateOpportunity, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToCreateOpportunity, http.StatusInternalServerError) return } @@ -202,5 +202,5 @@ func (h *Handler) CreateOpportunity(w http.ResponseWriter, r *http.Request) { UpdatedAt: opportunity.UpdatedAt.Format(time.RFC3339), } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } diff --git a/internal/interfaces/http/respond/respond.go b/internal/interfaces/http/respond/respond.go index 791518c..63348c4 100644 --- a/internal/interfaces/http/respond/respond.go +++ b/internal/interfaces/http/respond/respond.go @@ -1,4 +1,4 @@ -package responde +package respond import ( "encoding/json" diff --git a/internal/interfaces/http/respond/respond_test.go b/internal/interfaces/http/respond/respond_test.go index f669ab1..627f2c5 100644 --- a/internal/interfaces/http/respond/respond_test.go +++ b/internal/interfaces/http/respond/respond_test.go @@ -1,4 +1,4 @@ -package responde_test +package respond_test import ( "encoding/json" diff --git a/internal/interfaces/http/transaction.go b/internal/interfaces/http/transaction.go index af2608f..9d1453b 100644 --- a/internal/interfaces/http/transaction.go +++ b/internal/interfaces/http/transaction.go @@ -10,7 +10,7 @@ import ( "github.com/Financial-Partner/server/internal/entities" "github.com/Financial-Partner/server/internal/interfaces/http/dto" httperror "github.com/Financial-Partner/server/internal/interfaces/http/error" - responde "github.com/Financial-Partner/server/internal/interfaces/http/respond" + respond "github.com/Financial-Partner/server/internal/interfaces/http/respond" ) //go:generate mockgen -source=transaction.go -destination=transaction_mock.go -package=handler @@ -34,14 +34,14 @@ func (h *Handler) GetTransactions(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } transactions, err := h.transactionService.GetTransactions(r.Context(), userID) if err != nil { h.log.Errorf("failed to get transactions") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetTransactions, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetTransactions, http.StatusInternalServerError) return } @@ -62,7 +62,7 @@ func (h *Handler) GetTransactions(w http.ResponseWriter, r *http.Request) { Transactions: transactionResponses, } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } // @Summary Create a transaction @@ -81,21 +81,21 @@ func (h *Handler) CreateTransaction(w http.ResponseWriter, r *http.Request) { userID, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Warnf("failed to get user ID from context") - responde.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) + respond.WithError(w, r, h.log, nil, httperror.ErrUnauthorized, http.StatusUnauthorized) return } var req dto.CreateTransactionRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("failed to decode request body") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } transaction, err := h.transactionService.CreateTransaction(r.Context(), userID, &req) if err != nil { h.log.Errorf("failed to create transaction") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToCreateTransaction, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToCreateTransaction, http.StatusInternalServerError) return } @@ -109,5 +109,5 @@ func (h *Handler) CreateTransaction(w http.ResponseWriter, r *http.Request) { UpdatedAt: transaction.UpdatedAt.Format(time.RFC3339), } - responde.WithJSON(w, r, resp, http.StatusOK) + respond.WithJSON(w, r, resp, http.StatusOK) } diff --git a/internal/interfaces/http/user.go b/internal/interfaces/http/user.go index 3b68d49..f344344 100644 --- a/internal/interfaces/http/user.go +++ b/internal/interfaces/http/user.go @@ -10,7 +10,7 @@ import ( "github.com/Financial-Partner/server/internal/entities" "github.com/Financial-Partner/server/internal/interfaces/http/dto" httperror "github.com/Financial-Partner/server/internal/interfaces/http/error" - responde "github.com/Financial-Partner/server/internal/interfaces/http/respond" + respond "github.com/Financial-Partner/server/internal/interfaces/http/respond" ) //go:generate mockgen -source=user.go -destination=user_mock.go -package=handler @@ -39,21 +39,21 @@ func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { var req dto.UpdateUserRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.log.WithError(err).Warnf("Invalid request format") - responde.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) + respond.WithError(w, r, h.log, err, httperror.ErrInvalidRequest, http.StatusBadRequest) return } id, ok := contextutil.GetUserID(r.Context()) if !ok { h.log.Errorf("User ID not found in context") - responde.WithError(w, r, h.log, nil, httperror.ErrUserIDNotFound, http.StatusInternalServerError) + respond.WithError(w, r, h.log, nil, httperror.ErrUserIDNotFound, http.StatusInternalServerError) return } updatedUser, err := h.userService.UpdateUserName(r.Context(), id, req.Name) if err != nil { h.log.WithError(err).Errorf("Failed to update user") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToUpdateUser, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToUpdateUser, http.StatusInternalServerError) return } @@ -66,7 +66,7 @@ func (h *Handler) UpdateUser(w http.ResponseWriter, r *http.Request) { UpdatedAt: updatedUser.UpdatedAt.Format(time.RFC3339), } - responde.WithJSON(w, r, response, http.StatusOK) + respond.WithJSON(w, r, response, http.StatusOK) } // GetUser GetUser @@ -86,7 +86,7 @@ func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) { email, ok := contextutil.GetUserEmail(r.Context()) if !ok { h.log.Errorf("User email not found in context") - responde.WithError(w, r, h.log, nil, httperror.ErrEmailNotFound, http.StatusInternalServerError) + respond.WithError(w, r, h.log, nil, httperror.ErrEmailNotFound, http.StatusInternalServerError) return } @@ -99,13 +99,13 @@ func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) { userEntity, err := h.userService.GetUser(r.Context(), email) if err != nil { logger.WithError(err).Errorf("Failed to get user") - responde.WithError(w, r, h.log, err, httperror.ErrFailedToGetUser, http.StatusInternalServerError) + respond.WithError(w, r, h.log, err, httperror.ErrFailedToGetUser, http.StatusInternalServerError) return } response := buildUserResponse(userEntity, scopes) - responde.WithJSON(w, r, response, http.StatusOK) + respond.WithJSON(w, r, response, http.StatusOK) } func buildUserResponse(user *entities.User, scopes []string) dto.GetUserResponse { From a375767909fb5e3ae35f7883622a1ac05e6a6e96 Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Fri, 25 Apr 2025 12:59:36 +0800 Subject: [PATCH 4/4] Fix typo in MongoInvestmentRepository type name and update related method signatures --- .../persistence/mongodb/investment.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/infrastructure/persistence/mongodb/investment.go b/internal/infrastructure/persistence/mongodb/investment.go index f64f185..0e9e47f 100644 --- a/internal/infrastructure/persistence/mongodb/investment.go +++ b/internal/infrastructure/persistence/mongodb/investment.go @@ -9,17 +9,17 @@ import ( "github.com/Financial-Partner/server/internal/entities" ) -type MongoInvestmentResporitory struct { +type MongoInvestmentRepository struct { collection *mongo.Collection } -func NewInvestmentRepository(db MongoClient) *MongoInvestmentResporitory { - return &MongoInvestmentResporitory{ +func NewInvestmentRepository(db MongoClient) *MongoInvestmentRepository { + return &MongoInvestmentRepository{ collection: db.Collection("investments"), } } -func (r *MongoInvestmentResporitory) CreateInvestment(ctx context.Context, entity *entities.Investment) (*entities.Investment, error) { +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 @@ -27,7 +27,7 @@ func (r *MongoInvestmentResporitory) CreateInvestment(ctx context.Context, entit return entity, nil } -func (r *MongoInvestmentResporitory) CreateOpportunity(ctx context.Context, entity *entities.Opportunity) (*entities.Opportunity, error) { +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 @@ -35,7 +35,7 @@ func (r *MongoInvestmentResporitory) CreateOpportunity(ctx context.Context, enti return entity, nil } -func (r *MongoInvestmentResporitory) FindOpportunitiesByUserId(ctx context.Context, userID string) ([]entities.Opportunity, error) { +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 { @@ -50,7 +50,7 @@ func (r *MongoInvestmentResporitory) FindOpportunitiesByUserId(ctx context.Conte return opportunities, nil } -func (r *MongoInvestmentResporitory) FindInvestmentsByUserId(ctx context.Context, userID string) ([]entities.Investment, error) { +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 {