diff --git a/server/internal/pkg/dto/trip.go b/server/internal/pkg/dto/trip.go index 8a7087b..667dd35 100644 --- a/server/internal/pkg/dto/trip.go +++ b/server/internal/pkg/dto/trip.go @@ -9,10 +9,24 @@ import ( type TripResponseDTO struct { ID int `json:"id" example:"1"` Name string `json:"name" example:"sample-name"` - Budget float64 `json:"budget" example:"100.12"` + Budget float64 `json:"budget" example:"12345.12"` CountryProperty CountryResponseDTO `json:"countryProperty" binding:"required"` NoteProperty TripNoteProperty `json:"noteProperty"` Transaction []*TransactionResponseDTO `json:"transactions"` + StartDateTime time.Time `json:"startDateTime" example:"2024-01-02T15:04:05Z"` + EndDateTime time.Time `json:"endDateTime" example:"2024-01-02T15:04:05Z"` +} + +// Need to add top5 transactions +type DetailedTripResponseDTO struct { + ID int `json:"id" example:"1"` + Name string `json:"name" example:"sample-name"` + Budget float64 `json:"budget" example:"12345.12"` + CountryProperty CountryResponseDTO `json:"countryProperty" binding:"required"` + NoteProperty TripNoteProperty `json:"noteProperty"` + Transaction []*TransactionResponseDTO `json:"transactions"` + TotalExpense float64 `json:"totalExpense" example:"100.12"` + T5Transactions []*TransactionResponseDTO `json:"top5Transactions"` Description string `json:"description" example:"sample-description"` StartDateTime time.Time `json:"startDateTime" example:"2024-01-02T15:04:05Z"` EndDateTime time.Time `json:"endDateTime" example:"2024-01-02T15:04:05Z"` @@ -37,7 +51,7 @@ type TripNoteOptions struct { type TripRequestDTO struct { Name string `json:"name" binding:"required" example:"sample-name"` - Budget float64 `json:"budget" binding:"required" example:"2000.12"` + Budget float64 `json:"budget" binding:"required" example:"2000.02"` CountryId int `json:"countryId" binding:"required" example:"1"` Description string `json:"description" binding:"required" example:"sample-description"` NoteProperty TripNoteProperty `json:"noteProperty" binding:"required"` @@ -51,8 +65,23 @@ func NewTripResponse(trip *entities.Trip, country *entities.Country) *TripRespon Name: trip.Name(), Budget: trip.Budget(), CountryProperty: *NewCountryResponse(country), + NoteProperty: TripNoteProperty{NoteType: trip.Note().NoteType, NoteColor: trip.Note().NoteColor, BoundColor: trip.Note().BoundColor}, + Transaction: NewTransactionResponseList(trip.Transactions()), + StartDateTime: trip.StartDateTime(), + EndDateTime: trip.EndDateTime(), + } +} + +func NewDetailedTripResponse(trip *entities.Trip, country *entities.Country, totalExpense float64, top5Transactions []*entities.Transaction) *DetailedTripResponseDTO { + return &DetailedTripResponseDTO{ + ID: trip.ID(), + Name: trip.Name(), + Budget: trip.Budget(), + TotalExpense: totalExpense, + CountryProperty: *NewCountryResponse(country), Description: trip.Description(), NoteProperty: TripNoteProperty{NoteType: trip.Note().NoteType, NoteColor: trip.Note().NoteColor, BoundColor: trip.Note().BoundColor}, + T5Transactions: NewTransactionResponseList(top5Transactions), Transaction: NewTransactionResponseList(trip.Transactions()), StartDateTime: trip.StartDateTime(), EndDateTime: trip.EndDateTime(), diff --git a/server/internal/pkg/handlers/handler.go b/server/internal/pkg/handlers/handler.go index 87e047a..ba05fd3 100644 --- a/server/internal/pkg/handlers/handler.go +++ b/server/internal/pkg/handlers/handler.go @@ -37,6 +37,7 @@ func (h *Handler) InitRoutes() http.Handler { apiGroup.POST("/users", h.Register) apiGroup.POST("/users/login", h.Login) apiGroup.GET("/trips", middlewares.JwtAuthMiddleware(), h.GetTrip) + apiGroup.GET("/trips/:id", middlewares.JwtAuthMiddleware(), h.GetTripById) apiGroup.POST("/trips", middlewares.JwtAuthMiddleware(), h.RegisterTrip) apiGroup.DELETE("/trips/:id", middlewares.JwtAuthMiddleware(), h.DeleteTrip) apiGroup.PUT("/trips/:id", middlewares.JwtAuthMiddleware(), h.UpdateTrip) diff --git a/server/internal/pkg/handlers/trip.go b/server/internal/pkg/handlers/trip.go index 8f42267..dfb89ac 100644 --- a/server/internal/pkg/handlers/trip.go +++ b/server/internal/pkg/handlers/trip.go @@ -16,6 +16,7 @@ type TripUseCase interface { GetTripsByStatus(ctx context.Context, userId int) (*dto.TripStatusResponseDTO, error) DeleteTrip(ctx context.Context, tripId int) error UpdateTrip(ctx context.Context, tripId int, dto dto.TripRequestDTO) error + GetTripById(ctx context.Context, tripId int) (*dto.DetailedTripResponseDTO, error) } // register trip @@ -98,6 +99,51 @@ func (h *Handler) GetTrip(ctx *gin.Context) { ctx.JSON(http.StatusOK, trips) } +// get trip by id +// +// @Summary get trip by id +// @Description get trip by id +// @Tags trip +// @Accept json +// @Produce json +// @Security bearer +// @param Authorization header string true "Authorization" +// @Param id path int true "id" +// @Success 200 {object} dto.DetailedTripResponseDTO +// @Failure 400 {object} dto.ErrorResponseDTO +// @Failure 401 {object} dto.ErrorResponseDTO +// @Failure 500 {object} dto.ErrorResponseDTO +// @Router /v1/trips/{id} [get] +func (h *Handler) GetTripById(ctx *gin.Context) { + tripId := ctx.Param("id") + + if tripId == "" { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error_message": "bad request", + }) + return + } + + tripIdInt, err := strconv.Atoi(tripId) + if err != nil { + ctx.JSON(http.StatusBadRequest, gin.H{ + "error_message": "bad request", + }) + return + } + + trip, err := h.TripUseCase.GetTripById(ctx, tripIdInt) + + if err != nil { + ctx.JSON(http.StatusInternalServerError, gin.H{ + "error_message": err, + }) + return + } + + ctx.JSON(http.StatusOK, trip) +} + // delete trip // // @Summary delete trip diff --git a/server/internal/pkg/handlers/trip_test.go b/server/internal/pkg/handlers/trip_test.go index 95c6b9b..a84fa4c 100644 --- a/server/internal/pkg/handlers/trip_test.go +++ b/server/internal/pkg/handlers/trip_test.go @@ -100,6 +100,36 @@ func TestGetTrips(t *testing.T) { }) } +func TestGetTripById(t *testing.T) { + t.Run("successfully get the list of user's trip", func(t *testing.T) { + projectRootDir, _ := pathutil.GetProjectRootDir() + err := godotenv.Load(fmt.Sprintf("%s/.env", projectRootDir)) + assert.NoError(t, err) + + rr := httptest.NewRecorder() + tripUseCase := tripMocks.NewTripUseCase() + countryUseCase := countryMocks.NewCountryUseCase() + userUseCase := userMocks.NewUserUeseCase() + transactionUseCase := transactionMocks.NewTransactionUseCase() + handler := New(tripUseCase, countryUseCase, userUseCase, transactionUseCase) + router := handler.InitRoutes() + + userId := 1 + tripId := 1 + + token, err := token.MakeToken(userId) + assert.NoError(t, err) + + tripResponseDTO := dto.DetailedTripResponseDTO{} + tripUseCase.On("GetTripById", mock.Anything, tripId).Return(&tripResponseDTO, nil) + request, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/trips/%d", tripId), nil) + request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + router.ServeHTTP(rr, request) + assert.Equal(t, 200, rr.Code) + assert.NoError(t, err) + }) +} + func TestUpdateTrip(t *testing.T) { t.Run("successfully update user's trip", func(t *testing.T) { projectRootDir, _ := pathutil.GetProjectRootDir() diff --git a/server/internal/pkg/mocks/trip/usecase.go b/server/internal/pkg/mocks/trip/usecase.go index d64408b..779b2e8 100644 --- a/server/internal/pkg/mocks/trip/usecase.go +++ b/server/internal/pkg/mocks/trip/usecase.go @@ -74,3 +74,19 @@ func (m *TripUseCaseMock) GetTripsByStatus(ctx context.Context, userId int) (*dt return r0, r1 } + +func (m *TripUseCaseMock) GetTripById(ctx context.Context, tripId int) (*dto.DetailedTripResponseDTO, error) { + ret := m.Called(ctx, tripId) + + var r0 *dto.DetailedTripResponseDTO + if ret.Get(0) != nil { + r0 = ret.Get(0).(*dto.DetailedTripResponseDTO) + } + + var r1 error + if ret.Get(1) != nil { + r1 = ret.Get(1).(error) + } + + return r0, r1 +} diff --git a/server/internal/pkg/usecases/trip/usecase.go b/server/internal/pkg/usecases/trip/usecase.go index a242957..77d7c06 100644 --- a/server/internal/pkg/usecases/trip/usecase.go +++ b/server/internal/pkg/usecases/trip/usecase.go @@ -2,6 +2,7 @@ package usecase import ( "context" + "sort" "time" "github.com/Taehoya/pocket-mate/internal/pkg/dto" @@ -111,3 +112,46 @@ func (u *TripUseCase) UpdateTrip(ctx context.Context, tripId int, dto dto.TripRe return u.tripRepository.UpdateTrip(ctx, tripId, dto.Name, dto.Budget, dto.CountryId, dto.Description, *note, dto.StartDateTime, dto.EndDateTime) } + +func (u *TripUseCase) GetTripById(ctx context.Context, tripId int) (*dto.DetailedTripResponseDTO, error) { + trip, err := u.tripRepository.GetTripById(ctx, tripId) + if err != nil { + return nil, err + } + + country, err := u.countryRepository.GetCountryById(ctx, trip.CountryID()) + if err != nil { + return nil, err + } + + transactions, err := u.transactionRepository.GetTransactionByTripId(ctx, trip.ID()) + if err != nil { + return nil, err + } + trip.SetTransactions(transactions) + totalExpense := getTripsAndTotalExpense(transactions) + top5Transactions := getTopTransactions(transactions, 5) + tripResponse := dto.NewDetailedTripResponse(trip, country, totalExpense, top5Transactions) + return tripResponse, nil +} + +func getTripsAndTotalExpense(transactions []*entities.Transaction) float64 { + totalExpense := 0.0 + for _, transaction := range transactions { + totalExpense += transaction.Amount() + } + return totalExpense +} + +func getTopTransactions(transactionsParam []*entities.Transaction, top int) []*entities.Transaction { + transactions := transactionsParam + sort.Slice(transactions, func(i, j int) bool { + return transactions[i].Amount() > transactions[j].Amount() + }) + + if len(transactions) < top { + return transactions + } + + return transactions[:top] +} diff --git a/server/internal/pkg/usecases/trip/usecase_test.go b/server/internal/pkg/usecases/trip/usecase_test.go index 3c88bac..5a237d1 100644 --- a/server/internal/pkg/usecases/trip/usecase_test.go +++ b/server/internal/pkg/usecases/trip/usecase_test.go @@ -166,6 +166,84 @@ func TestGetTrips(t *testing.T) { }) } +func TestGetTripById(t *testing.T) { + t.Run("successfully get trip by id", func(t *testing.T) { + tripRepository := tripMock.NewTripRepositoryMock() + userTripRepository := userTripMock.NewUserTripRepositoryMock() + countryRepository := countryMock.NewCountryRepositoryMock() + TransactionRepository := transactionMock.NewTransactionRepositoryMock() + usecase := NewTripUseCase(tripRepository, userTripRepository, countryRepository, TransactionRepository) + + ctx := context.TODO() + tripId := 1 + + trip := entities.NewTrip(tripId, "test-name", 1000, true, 1, "test-description", entities.Note{}, time.Now(), time.Now(), time.Now(), time.Now()) + tripRepository.Mock.On("GetTripById", ctx, tripId).Return(trip, nil) + countryRepository.Mock.On("GetCountryById", ctx, trip.CountryID()).Return(&entities.Country{}, nil) + TransactionRepository.Mock.On("GetTransactionByTripId", ctx, tripId).Return([]*entities.Transaction{}, nil) + + _, err := usecase.GetTripById(ctx, tripId) + assert.NoError(t, err) + tripRepository.AssertExpectations(t) + }) + + t.Run("failed to get trip", func(t *testing.T) { + tripRepository := tripMock.NewTripRepositoryMock() + userTripRepository := userTripMock.NewUserTripRepositoryMock() + countryRepository := countryMock.NewCountryRepositoryMock() + TransactionRepository := transactionMock.NewTransactionRepositoryMock() + usecase := NewTripUseCase(tripRepository, userTripRepository, countryRepository, TransactionRepository) + + ctx := context.TODO() + tripId := 1 + + trip := entities.NewTrip(tripId, "test-name", 1000, true, 1, "test-description", entities.Note{}, time.Now(), time.Now(), time.Now(), time.Now()) + tripRepository.Mock.On("GetTripById", ctx, tripId).Return(trip, fmt.Errorf("error")) + _, err := usecase.GetTripById(ctx, tripId) + assert.Error(t, err) + tripRepository.AssertExpectations(t) + }) + + t.Run("failed to get trip", func(t *testing.T) { + tripRepository := tripMock.NewTripRepositoryMock() + userTripRepository := userTripMock.NewUserTripRepositoryMock() + countryRepository := countryMock.NewCountryRepositoryMock() + TransactionRepository := transactionMock.NewTransactionRepositoryMock() + usecase := NewTripUseCase(tripRepository, userTripRepository, countryRepository, TransactionRepository) + + ctx := context.TODO() + tripId := 1 + + trip := entities.NewTrip(tripId, "test-name", 1000, true, 1, "test-description", entities.Note{}, time.Now(), time.Now(), time.Now(), time.Now()) + tripRepository.Mock.On("GetTripById", ctx, tripId).Return(trip, nil) + countryRepository.Mock.On("GetCountryById", ctx, trip.CountryID()).Return(nil, fmt.Errorf("error")) + + _, err := usecase.GetTripById(ctx, tripId) + assert.Error(t, err) + tripRepository.AssertExpectations(t) + }) + + t.Run("failed to get trip", func(t *testing.T) { + tripRepository := tripMock.NewTripRepositoryMock() + userTripRepository := userTripMock.NewUserTripRepositoryMock() + countryRepository := countryMock.NewCountryRepositoryMock() + TransactionRepository := transactionMock.NewTransactionRepositoryMock() + usecase := NewTripUseCase(tripRepository, userTripRepository, countryRepository, TransactionRepository) + + ctx := context.TODO() + tripId := 1 + + trip := entities.NewTrip(tripId, "test-name", 1000, true, 1, "test-description", entities.Note{}, time.Now(), time.Now(), time.Now(), time.Now()) + tripRepository.Mock.On("GetTripById", ctx, tripId).Return(trip, nil) + countryRepository.Mock.On("GetCountryById", ctx, trip.CountryID()).Return(&entities.Country{}, nil) + TransactionRepository.Mock.On("GetTransactionByTripId", ctx, tripId).Return(nil, fmt.Errorf("error")) + + _, err := usecase.GetTripById(ctx, tripId) + assert.Error(t, err) + tripRepository.AssertExpectations(t) + }) +} + func TestDeleteTrip(t *testing.T) { t.Run("successfully delete trip", func(t *testing.T) { tripRepository := tripMock.NewTripRepositoryMock()