From 5d6c54741d1cfc7c56e6ff874d09871046f44a4c Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Sat, 29 Mar 2025 16:34:40 +0800 Subject: [PATCH 1/2] Add Gacha service and related HTTP handlers for drawing and previewing gachas --- cmd/server/providers.go | 8 +- cmd/server/route.go | 10 +- cmd/server/wire.go | 1 + cmd/server/wire_gen.go | 3 +- internal/entities/gacha.go | 10 + internal/interfaces/http/dto/gacha.go | 14 ++ internal/interfaces/http/error/error.go | 2 + internal/interfaces/http/gacha.go | 100 ++++++++++ internal/interfaces/http/gacha_mock.go | 73 ++++++++ internal/interfaces/http/gacha_test.go | 227 +++++++++++++++++++++++ internal/interfaces/http/handler.go | 4 +- internal/interfaces/http/handler_test.go | 4 +- internal/module/gacha/usecase/service.go | 23 +++ swagger/docs.go | 133 +++++++++++++ swagger/swagger.json | 133 +++++++++++++ swagger/swagger.yaml | 88 +++++++++ 16 files changed, 826 insertions(+), 7 deletions(-) create mode 100644 internal/entities/gacha.go create mode 100644 internal/interfaces/http/dto/gacha.go create mode 100644 internal/interfaces/http/gacha.go create mode 100644 internal/interfaces/http/gacha_mock.go create mode 100644 internal/interfaces/http/gacha_test.go create mode 100644 internal/module/gacha/usecase/service.go diff --git a/cmd/server/providers.go b/cmd/server/providers.go index 5171953..cd1a5dd 100644 --- a/cmd/server/providers.go +++ b/cmd/server/providers.go @@ -16,6 +16,7 @@ import ( handler "github.com/Financial-Partner/server/internal/interfaces/http" "github.com/Financial-Partner/server/internal/interfaces/http/middleware" auth_usecase "github.com/Financial-Partner/server/internal/module/auth/usecase" + gacha_usecase "github.com/Financial-Partner/server/internal/module/gacha/usecase" goal_usecase "github.com/Financial-Partner/server/internal/module/goal/usecase" investment_usecase "github.com/Financial-Partner/server/internal/module/investment/usecase" transaction_usecase "github.com/Financial-Partner/server/internal/module/transaction/usecase" @@ -86,15 +87,20 @@ func ProvideTransactionService() *transaction_usecase.Service { return transaction_usecase.NewService() } +func ProvideGachaService() *gacha_usecase.Service { + return gacha_usecase.NewService() +} + func ProvideHandler( userService *user_usecase.Service, authService *auth_usecase.Service, goalService *goal_usecase.Service, investmentService *investment_usecase.Service, transactionService *transaction_usecase.Service, + gachaService *gacha_usecase.Service, log loggerInfra.Logger, ) *handler.Handler { - return handler.NewHandler(userService, authService, goalService, investmentService, transactionService, log) + return handler.NewHandler(userService, authService, goalService, investmentService, transactionService, gachaService, log) } func ProvideAuthMiddleware(jwtManager *authInfra.JWTManager, cfg *config.Config, log loggerInfra.Logger) *middleware.AuthMiddleware { diff --git a/cmd/server/route.go b/cmd/server/route.go index 0a5c119..eb5ea33 100644 --- a/cmd/server/route.go +++ b/cmd/server/route.go @@ -62,11 +62,15 @@ func setupProtectedRoutes(router *mux.Router, handlers *handler.Handler) { investmentRoutes := router.PathPrefix("/investments").Subrouter() investmentRoutes.HandleFunc("", handlers.GetOpportunities).Methods(http.MethodGet) - userInvestmentRoute := router.PathPrefix("/users/me/investment").Subrouter() - userInvestmentRoute.HandleFunc("/", handlers.CreateUserInvestment).Methods(http.MethodPost) - userInvestmentRoute.HandleFunc("/", handlers.GetUserInvestments).Methods(http.MethodGet) + userInvestmentRoutes := router.PathPrefix("/users/me/investment").Subrouter() + userInvestmentRoutes.HandleFunc("/", handlers.CreateUserInvestment).Methods(http.MethodPost) + userInvestmentRoutes.HandleFunc("/", handlers.GetUserInvestments).Methods(http.MethodGet) transactionRoutes := router.PathPrefix("/transactions").Subrouter() transactionRoutes.HandleFunc("", handlers.CreateTransaction).Methods(http.MethodPost) transactionRoutes.HandleFunc("", handlers.GetTransactions).Methods(http.MethodGet) + + gachaRoutes := router.PathPrefix("/gacha").Subrouter() + gachaRoutes.HandleFunc("/draw", handlers.DrawGacha).Methods(http.MethodPost) + gachaRoutes.HandleFunc("/preview", handlers.PreviewGachas).Methods(http.MethodGet) } diff --git a/cmd/server/wire.go b/cmd/server/wire.go index 8e3cfb4..1a27797 100644 --- a/cmd/server/wire.go +++ b/cmd/server/wire.go @@ -24,6 +24,7 @@ func InitializeServer(cfgFile string) (*Server, error) { ProvideGoalService, ProvideInvestmentService, ProvideTransactionService, + ProvideGachaService, ProvideHandler, ProvideAuthMiddleware, ProvideRouter, diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index aad67de..64bbfa7 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -39,7 +39,8 @@ func InitializeServer(cfgFile string) (*Server, error) { goal_usecaseService := ProvideGoalService() investment_usecaseService := ProvideInvestmentService() transaction_usecaseService := ProvideTransactionService() - handler := ProvideHandler(service, auth_usecaseService, goal_usecaseService, investment_usecaseService, transaction_usecaseService, logger) + gacha_usecaseService := ProvideGachaService() + handler := ProvideHandler(service, auth_usecaseService, goal_usecaseService, investment_usecaseService, transaction_usecaseService, gacha_usecaseService, logger) authMiddleware := ProvideAuthMiddleware(jwtManager, config, logger) loggerMiddleware := ProvideLoggerMiddleware(logger) router := ProvideRouter(handler, authMiddleware, loggerMiddleware, config) diff --git a/internal/entities/gacha.go b/internal/entities/gacha.go new file mode 100644 index 0000000..7a4e471 --- /dev/null +++ b/internal/entities/gacha.go @@ -0,0 +1,10 @@ +package entities + +import ( + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Gacha struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id"` + ImgSrc string `bson:"img_src" json:"img_src"` +} diff --git a/internal/interfaces/http/dto/gacha.go b/internal/interfaces/http/dto/gacha.go new file mode 100644 index 0000000..62aa4a1 --- /dev/null +++ b/internal/interfaces/http/dto/gacha.go @@ -0,0 +1,14 @@ +package dto + +type GachaResponse struct { + ID string `json:"id" example:"60d6ec33f777b123e4567890"` + ImgSrc string `json:"img_src" example:"https://example.com/image.png"` +} + +type DrawGachaRequest struct { + Amount int64 `json:"amount" example:"100" binding:"required"` +} + +type PreviewGachasResponse struct { + Gachas []GachaResponse `json:"gachas"` +} diff --git a/internal/interfaces/http/error/error.go b/internal/interfaces/http/error/error.go index 1e3ca00..ea56c5d 100644 --- a/internal/interfaces/http/error/error.go +++ b/internal/interfaces/http/error/error.go @@ -22,4 +22,6 @@ const ( ErrFailedToGetUserInvestments = "Failed to get user investments" ErrFailedToGetTransactions = "Failed to get transactions" ErrFailedToCreateTransaction = "Failed to create a transaction" + ErrFailedToDrawGacha = "Failed to draw a gacha" + ErrFailedToPreviewGachas = "Failed to preview gachas" ) diff --git a/internal/interfaces/http/gacha.go b/internal/interfaces/http/gacha.go new file mode 100644 index 0000000..f62d732 --- /dev/null +++ b/internal/interfaces/http/gacha.go @@ -0,0 +1,100 @@ +package handler + +import ( + "context" + "encoding/json" + "net/http" + + "github.com/Financial-Partner/server/internal/contextutil" + "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" +) + +//go:generate mockgen -source=gacha.go -destination=gacha_mock.go -package=handler + +type GachaService interface { + DrawGacha(ctx context.Context, userID string, req *dto.DrawGachaRequest) (*entities.Gacha, error) + PreviewGachas(ctx context.Context, userID string) ([]entities.Gacha, error) +} + +// @Summary Decrease user's gacha amount and return gacha result +// @Description Decrease user's gacha amount and return gacha result +// @Tags gacha +// @Accept json +// @Produce json +// @Param request body dto.DrawGachaRequest true "Draw gacha request" +// @Param Authorization header string true "Bearer {token}" default "Bearer " +// @Success 200 {object} dto.GachaResponse +// @Failure 401 {object} dto.ErrorResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /gacha/draw [post] +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) + 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) + 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) + return + } + + resp := dto.GachaResponse{ + ID: gacha.ID.Hex(), + ImgSrc: gacha.ImgSrc, + } + + responde.WithJSON(w, r, resp, http.StatusOK) +} + +// @Summary Get 9 gacha images for preview +// @Description Get 9 gacha images for preview +// @Tags gacha +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer {token}" default "Bearer " +// @Success 200 {object} dto.PreviewGachasResponse +// @Failure 401 {object} dto.ErrorResponse +// @Failure 500 {object} dto.ErrorResponse +// @Router /gacha/preview [get] +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) + 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) + return + } + + resp := dto.PreviewGachasResponse{ + Gachas: make([]dto.GachaResponse, 0, 9), + } + + for _, gacha := range gachas { + resp.Gachas = append(resp.Gachas, dto.GachaResponse{ + ID: gacha.ID.Hex(), + ImgSrc: gacha.ImgSrc, + }) + } + + responde.WithJSON(w, r, resp, http.StatusOK) +} diff --git a/internal/interfaces/http/gacha_mock.go b/internal/interfaces/http/gacha_mock.go new file mode 100644 index 0000000..3dd473f --- /dev/null +++ b/internal/interfaces/http/gacha_mock.go @@ -0,0 +1,73 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: gacha.go +// +// Generated by this command: +// +// mockgen -source=gacha.go -destination=gacha_mock.go -package=handler +// + +// Package handler is a generated GoMock package. +package handler + +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" +) + +// MockGachaService is a mock of GachaService interface. +type MockGachaService struct { + ctrl *gomock.Controller + recorder *MockGachaServiceMockRecorder + isgomock struct{} +} + +// MockGachaServiceMockRecorder is the mock recorder for MockGachaService. +type MockGachaServiceMockRecorder struct { + mock *MockGachaService +} + +// NewMockGachaService creates a new mock instance. +func NewMockGachaService(ctrl *gomock.Controller) *MockGachaService { + mock := &MockGachaService{ctrl: ctrl} + mock.recorder = &MockGachaServiceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockGachaService) EXPECT() *MockGachaServiceMockRecorder { + return m.recorder +} + +// DrawGacha mocks base method. +func (m *MockGachaService) DrawGacha(ctx context.Context, userID string, req *dto.DrawGachaRequest) (*entities.Gacha, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DrawGacha", ctx, userID, req) + ret0, _ := ret[0].(*entities.Gacha) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DrawGacha indicates an expected call of DrawGacha. +func (mr *MockGachaServiceMockRecorder) DrawGacha(ctx, userID, req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DrawGacha", reflect.TypeOf((*MockGachaService)(nil).DrawGacha), ctx, userID, req) +} + +// PreviewGachas mocks base method. +func (m *MockGachaService) PreviewGachas(ctx context.Context, userID string) ([]entities.Gacha, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PreviewGachas", ctx, userID) + ret0, _ := ret[0].([]entities.Gacha) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PreviewGachas indicates an expected call of PreviewGachas. +func (mr *MockGachaServiceMockRecorder) PreviewGachas(ctx, userID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreviewGachas", reflect.TypeOf((*MockGachaService)(nil).PreviewGachas), ctx, userID) +} diff --git a/internal/interfaces/http/gacha_test.go b/internal/interfaces/http/gacha_test.go new file mode 100644 index 0000000..0b3845c --- /dev/null +++ b/internal/interfaces/http/gacha_test.go @@ -0,0 +1,227 @@ +package handler_test + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Financial-Partner/server/internal/contextutil" + "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" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/mock/gomock" +) + +func TestDrawGacha(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + 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", "/gacha/draw", invalidBody) + r = r.WithContext(ctx) + + h.DrawGacha(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("Unauthorized request", func(t *testing.T) { + h, _ := newTestHandler(t) + + req := dto.DrawGachaRequest{ + Amount: 100, + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/gacha/draw", bytes.NewBuffer(body)) + + h.DrawGacha(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("Service error", func(t *testing.T) { + h, mockServices := newTestHandler(t) + + userID := primitive.NewObjectID().Hex() + userEmail := "test@example.com" + + mockServices.GachaService.EXPECT(). + DrawGacha(gomock.Any(), userID, gomock.Any()). + Return(nil, errors.New("service error")) + + req := dto.DrawGachaRequest{ + Amount: 100, + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/gacha/draw", bytes.NewBuffer(body)) + ctx := newContext(userID, userEmail) + r = r.WithContext(ctx) + + h.DrawGacha(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.ErrFailedToDrawGacha, errorResp.Message) + }) + + t.Run("Success", func(t *testing.T) { + h, mockServices := newTestHandler(t) + + userID := primitive.NewObjectID().Hex() + userEmail := "test@example.com" + + objectID := primitive.NewObjectID() + gacha := &entities.Gacha{ + ID: objectID, + ImgSrc: "https://example.com/image.png", + } + + mockServices.GachaService.EXPECT(). + DrawGacha(gomock.Any(), userID, gomock.Any()). + Return(gacha, nil) + + req := dto.DrawGachaRequest{ + Amount: 100, + } + body, _ := json.Marshal(req) + w := httptest.NewRecorder() + r := httptest.NewRequest("POST", "/gacha/draw", bytes.NewBuffer(body)) + ctx := newContext(userID, userEmail) + r = r.WithContext(ctx) + + h.DrawGacha(w, r) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var response dto.GachaResponse + err := json.NewDecoder(w.Body).Decode(&response) + assert.NoError(t, err) + assert.Equal(t, gacha.ID.Hex(), response.ID) + assert.Equal(t, gacha.ImgSrc, response.ImgSrc) + }) +} + +func TestPreviewGacha(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + t.Run("Unauthorized request", func(t *testing.T) { + h, _ := newTestHandler(t) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/gacha/preview", nil) + + h.PreviewGachas(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("Service error", func(t *testing.T) { + h, mockServices := newTestHandler(t) + + userID := primitive.NewObjectID().Hex() + userEmail := "test@example.com" + + mockServices.GachaService.EXPECT(). + PreviewGachas(gomock.Any(), userID). + Return(nil, errors.New("service error")) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/gacha/preview", nil) + ctx := newContext(userID, userEmail) + r = r.WithContext(ctx) + + h.PreviewGachas(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.ErrFailedToPreviewGachas, errorResp.Message) + }) + + t.Run("Success", func(t *testing.T) { + h, mockServices := newTestHandler(t) + + userID := primitive.NewObjectID().Hex() + userEmail := "test@example.com" + + gachas := make([]entities.Gacha, 9) // Create a slice for 10 Gacha objects + for i := 0; i < 9; i++ { + objectID := primitive.NewObjectID() + gachas[i] = entities.Gacha{ + ID: objectID, + ImgSrc: fmt.Sprintf("https://example.com/image%d.png", i+1), + } + } + + mockServices.GachaService.EXPECT(). + PreviewGachas(gomock.Any(), userID). + Return(gachas, nil) + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/gacha/preview", nil) + ctx := newContext(userID, userEmail) + r = r.WithContext(ctx) + + h.PreviewGachas(w, r) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var response dto.PreviewGachasResponse + err := json.NewDecoder(w.Body).Decode(&response) + assert.NoError(t, err) + assert.Len(t, response.Gachas, 9) // Check if the length of the Gachas slice is 10 + for i, gacha := range gachas { + assert.Equal(t, gacha.ID.Hex(), response.Gachas[i].ID) + assert.Equal(t, gacha.ImgSrc, response.Gachas[i].ImgSrc) + } + }) +} diff --git a/internal/interfaces/http/handler.go b/internal/interfaces/http/handler.go index 5130970..c522ef9 100644 --- a/internal/interfaces/http/handler.go +++ b/internal/interfaces/http/handler.go @@ -10,16 +10,18 @@ type Handler struct { goalService GoalService investmentService InvestmentService transactionService TransactionService + gachaService GachaService log logger.Logger } -func NewHandler(us UserService, as AuthService, gs GoalService, is InvestmentService, ts TransactionService, log logger.Logger) *Handler { +func NewHandler(us UserService, as AuthService, gs GoalService, is InvestmentService, ts TransactionService, gcs GachaService, log logger.Logger) *Handler { return &Handler{ userService: us, authService: as, goalService: gs, investmentService: is, transactionService: ts, + gachaService: gcs, log: log, } } diff --git a/internal/interfaces/http/handler_test.go b/internal/interfaces/http/handler_test.go index 1d6175f..01a7c76 100644 --- a/internal/interfaces/http/handler_test.go +++ b/internal/interfaces/http/handler_test.go @@ -17,6 +17,7 @@ type MockServices struct { GoalService *handler.MockGoalService InvestmentService *handler.MockInvestmentService TransactionService *handler.MockTransactionService + GachaService *handler.MockGachaService } func newTestHandler(t *testing.T) (*handler.Handler, *MockServices) { @@ -29,8 +30,9 @@ func newTestHandler(t *testing.T) (*handler.Handler, *MockServices) { GoalService: handler.NewMockGoalService(ctrl), InvestmentService: handler.NewMockInvestmentService(ctrl), TransactionService: handler.NewMockTransactionService(ctrl), + GachaService: handler.NewMockGachaService(ctrl), } - h := handler.NewHandler(ms.UserService, ms.AuthService, ms.GoalService, ms.InvestmentService, ms.TransactionService, logger.NewNopLogger()) + h := handler.NewHandler(ms.UserService, ms.AuthService, ms.GoalService, ms.InvestmentService, ms.TransactionService, ms.GachaService, logger.NewNopLogger()) return h, ms } diff --git a/internal/module/gacha/usecase/service.go b/internal/module/gacha/usecase/service.go new file mode 100644 index 0000000..b0c859f --- /dev/null +++ b/internal/module/gacha/usecase/service.go @@ -0,0 +1,23 @@ +package gacha_usecase + +import ( + "context" + + "github.com/Financial-Partner/server/internal/entities" + "github.com/Financial-Partner/server/internal/interfaces/http/dto" +) + +type Service struct { +} + +func NewService() *Service { + return &Service{} +} + +func (s *Service) PreviewGachas(ctx context.Context, userID string) ([]entities.Gacha, error) { + return nil, nil +} + +func (s *Service) DrawGacha(ctx context.Context, userID string, req *dto.DrawGachaRequest) (*entities.Gacha, error) { + return nil, nil +} diff --git a/swagger/docs.go b/swagger/docs.go index 90cec01..ab74d58 100644 --- a/swagger/docs.go +++ b/swagger/docs.go @@ -165,6 +165,103 @@ const docTemplate = `{ } } }, + "/gacha/draw": { + "post": { + "description": "Decrease user's gacha amount and return gacha result", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "gacha" + ], + "summary": "Decrease user's gacha amount and return gacha result", + "parameters": [ + { + "description": "Draw gacha request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DrawGachaRequest" + } + }, + { + "type": "string", + "description": "Bearer {token}", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.GachaResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/gacha/preview": { + "get": { + "description": "Get 9 gacha images for preview", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "gacha" + ], + "summary": "Get 9 gacha images for preview", + "parameters": [ + { + "type": "string", + "description": "Bearer {token}", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PreviewGachasResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, "/goals": { "get": { "description": "Get user's current saving goal and status", @@ -823,6 +920,18 @@ const docTemplate = `{ } } }, + "dto.DrawGachaRequest": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "type": "integer", + "example": 100 + } + } + }, "dto.ErrorResponse": { "type": "object", "properties": { @@ -834,6 +943,19 @@ const docTemplate = `{ } } }, + "dto.GachaResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "60d6ec33f777b123e4567890" + }, + "img_src": { + "type": "string", + "example": "https://example.com/image.png" + } + } + }, "dto.GetGoalResponse": { "type": "object", "properties": { @@ -1130,6 +1252,17 @@ const docTemplate = `{ } } }, + "dto.PreviewGachasResponse": { + "type": "object", + "properties": { + "gachas": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.GachaResponse" + } + } + } + }, "dto.RefreshTokenRequest": { "type": "object", "required": [ diff --git a/swagger/swagger.json b/swagger/swagger.json index 395104b..45686de 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -158,6 +158,103 @@ } } }, + "/gacha/draw": { + "post": { + "description": "Decrease user's gacha amount and return gacha result", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "gacha" + ], + "summary": "Decrease user's gacha amount and return gacha result", + "parameters": [ + { + "description": "Draw gacha request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.DrawGachaRequest" + } + }, + { + "type": "string", + "description": "Bearer {token}", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.GachaResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, + "/gacha/preview": { + "get": { + "description": "Get 9 gacha images for preview", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "gacha" + ], + "summary": "Get 9 gacha images for preview", + "parameters": [ + { + "type": "string", + "description": "Bearer {token}", + "name": "Authorization", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.PreviewGachasResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/dto.ErrorResponse" + } + } + } + } + }, "/goals": { "get": { "description": "Get user's current saving goal and status", @@ -816,6 +913,18 @@ } } }, + "dto.DrawGachaRequest": { + "type": "object", + "required": [ + "amount" + ], + "properties": { + "amount": { + "type": "integer", + "example": 100 + } + } + }, "dto.ErrorResponse": { "type": "object", "properties": { @@ -827,6 +936,19 @@ } } }, + "dto.GachaResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "example": "60d6ec33f777b123e4567890" + }, + "img_src": { + "type": "string", + "example": "https://example.com/image.png" + } + } + }, "dto.GetGoalResponse": { "type": "object", "properties": { @@ -1123,6 +1245,17 @@ } } }, + "dto.PreviewGachasResponse": { + "type": "object", + "properties": { + "gachas": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.GachaResponse" + } + } + } + }, "dto.RefreshTokenRequest": { "type": "object", "required": [ diff --git a/swagger/swagger.yaml b/swagger/swagger.yaml index ebfd924..3c150a6 100644 --- a/swagger/swagger.yaml +++ b/swagger/swagger.yaml @@ -65,6 +65,14 @@ definitions: investment: $ref: '#/definitions/dto.InvestmentResponse' type: object + dto.DrawGachaRequest: + properties: + amount: + example: 100 + type: integer + required: + - amount + type: object dto.ErrorResponse: properties: code: @@ -72,6 +80,15 @@ definitions: message: type: string type: object + dto.GachaResponse: + properties: + id: + example: 60d6ec33f777b123e4567890 + type: string + img_src: + example: https://example.com/image.png + type: string + type: object dto.GetGoalResponse: properties: goal: @@ -282,6 +299,13 @@ definitions: example: 20 type: integer type: object + dto.PreviewGachasResponse: + properties: + gachas: + items: + $ref: '#/definitions/dto.GachaResponse' + type: array + type: object dto.RefreshTokenRequest: properties: refresh_token: @@ -495,6 +519,70 @@ paths: summary: Refresh Access Token tags: - auth + /gacha/draw: + post: + consumes: + - application/json + description: Decrease user's gacha amount and return gacha result + parameters: + - description: Draw gacha request + in: body + name: request + required: true + schema: + $ref: '#/definitions/dto.DrawGachaRequest' + - description: Bearer {token} + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.GachaResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/dto.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/dto.ErrorResponse' + summary: Decrease user's gacha amount and return gacha result + tags: + - gacha + /gacha/preview: + get: + consumes: + - application/json + description: Get 9 gacha images for preview + parameters: + - description: Bearer {token} + in: header + name: Authorization + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.PreviewGachasResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/dto.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/dto.ErrorResponse' + summary: Get 9 gacha images for preview + tags: + - gacha /goals: get: consumes: From 0615460a58bb1fe2e65013d17b19736ce8f33a3a Mon Sep 17 00:00:00 2001 From: jyx0615 Date: Tue, 8 Apr 2025 17:43:55 +0800 Subject: [PATCH 2/2] Fix test assertions and comments for Gacha preview to reflect correct slice size --- internal/interfaces/http/gacha_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/interfaces/http/gacha_test.go b/internal/interfaces/http/gacha_test.go index 0b3845c..b11eb92 100644 --- a/internal/interfaces/http/gacha_test.go +++ b/internal/interfaces/http/gacha_test.go @@ -192,7 +192,7 @@ func TestPreviewGacha(t *testing.T) { userID := primitive.NewObjectID().Hex() userEmail := "test@example.com" - gachas := make([]entities.Gacha, 9) // Create a slice for 10 Gacha objects + gachas := make([]entities.Gacha, 9) // Create a slice for 9 Gacha objects for i := 0; i < 9; i++ { objectID := primitive.NewObjectID() gachas[i] = entities.Gacha{ @@ -218,7 +218,7 @@ func TestPreviewGacha(t *testing.T) { var response dto.PreviewGachasResponse err := json.NewDecoder(w.Body).Decode(&response) assert.NoError(t, err) - assert.Len(t, response.Gachas, 9) // Check if the length of the Gachas slice is 10 + assert.Len(t, response.Gachas, 9) // Check if the length of the Gachas slice is 9 for i, gacha := range gachas { assert.Equal(t, gacha.ID.Hex(), response.Gachas[i].ID) assert.Equal(t, gacha.ImgSrc, response.Gachas[i].ImgSrc)