From 8f475d1a0b367ac21708c4d0b55260692bd2ece2 Mon Sep 17 00:00:00 2001 From: pahmiudahgede Date: Wed, 19 Feb 2025 01:21:55 +0700 Subject: [PATCH] feat: add feature initialcoin --- config/database.go | 1 + dto/initialcoint_dto.go | 34 +++ internal/handler/initialcoint_handler.go | 98 +++++++ internal/repositories/initialcoint_repo.go | 67 +++++ internal/services/initialcoint_service.go | 299 +++++++++++++++++++++ model/initialcoint_model.go | 11 + presentation/initialcoint_route.go | 27 ++ router/setup_routes.go.go | 1 + utils/redis_caching.go | 4 + 9 files changed, 542 insertions(+) create mode 100644 dto/initialcoint_dto.go create mode 100644 internal/handler/initialcoint_handler.go create mode 100644 internal/repositories/initialcoint_repo.go create mode 100644 internal/services/initialcoint_service.go create mode 100644 model/initialcoint_model.go create mode 100644 presentation/initialcoint_route.go diff --git a/config/database.go b/config/database.go index d782dad..03609b7 100644 --- a/config/database.go +++ b/config/database.go @@ -45,6 +45,7 @@ func ConnectDatabase() { &model.Address{}, &model.Article{}, &model.Banner{}, + &model.InitialCoint{}, // ==main feature== ) if err != nil { diff --git a/dto/initialcoint_dto.go b/dto/initialcoint_dto.go new file mode 100644 index 0000000..06d1fb2 --- /dev/null +++ b/dto/initialcoint_dto.go @@ -0,0 +1,34 @@ +package dto + +import "strings" + +type ReponseInitialCointDTO struct { + ID string `json:"coin_id"` + CoinName string `json:"coin_name"` + ValuePerUnit float64 `json:"value_perunit"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +type RequestInitialCointDTO struct { + CoinName string `json:"coin_name"` + ValuePerUnit float64 `json:"value_perunit"` +} + +func (r *RequestInitialCointDTO) ValidateCointInput() (map[string][]string, bool) { + errors := make(map[string][]string) + + if strings.TrimSpace(r.CoinName) == "" { + errors["coin_name"] = append(errors["coin_name"], "nama coin harus diisi") + } + + if r.ValuePerUnit <= 0 { + errors["value_perunit"] = append(errors["value_perunit"], "value per unit harus lebih besar dari 0") + } + + if len(errors) > 0 { + return errors, false + } + + return nil, true +} diff --git a/internal/handler/initialcoint_handler.go b/internal/handler/initialcoint_handler.go new file mode 100644 index 0000000..2cf275a --- /dev/null +++ b/internal/handler/initialcoint_handler.go @@ -0,0 +1,98 @@ +package handler + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type InitialCointHandler struct { + InitialCointService services.InitialCointService +} + +func NewInitialCointHandler(initialCointService services.InitialCointService) *InitialCointHandler { + return &InitialCointHandler{InitialCointService: initialCointService} +} + +func (h *InitialCointHandler) CreateInitialCoint(c *fiber.Ctx) error { + var request dto.RequestInitialCointDTO + + if err := c.BodyParser(&request); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.ValidateCointInput() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + initialCointResponse, err := h.InitialCointService.CreateInitialCoint(request) + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, err.Error()) + } + + return utils.CreateResponse(c, initialCointResponse, "Initial coint created successfully") +} + +func (h *InitialCointHandler) GetAllInitialCoints(c *fiber.Ctx) error { + initialCoints, err := h.InitialCointService.GetAllInitialCoints() + if err != nil { + return utils.GenericResponse(c, fiber.StatusInternalServerError, "Failed to fetch initial coints") + } + + return utils.NonPaginatedResponse(c, initialCoints, len(initialCoints), "Initial coints fetched successfully") +} + +func (h *InitialCointHandler) GetInitialCointByID(c *fiber.Ctx) error { + id := c.Params("coin_id") + if id == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Coin ID is required") + } + + initialCoint, err := h.InitialCointService.GetInitialCointByID(id) + if err != nil { + return utils.GenericResponse(c, fiber.StatusNotFound, "Invalid coin ID") + } + + return utils.SuccessResponse(c, initialCoint, "Initial coint fetched successfully") +} + +func (h *InitialCointHandler) UpdateInitialCoint(c *fiber.Ctx) error { + id := c.Params("coin_id") + if id == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Coin ID is required") + } + + var request dto.RequestInitialCointDTO + + if err := c.BodyParser(&request); err != nil { + return utils.ValidationErrorResponse(c, map[string][]string{"body": {"Invalid body"}}) + } + + errors, valid := request.ValidateCointInput() + if !valid { + return utils.ValidationErrorResponse(c, errors) + } + + initialCointResponse, err := h.InitialCointService.UpdateInitialCoint(id, request) + if err != nil { + return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + } + + return utils.SuccessResponse(c, initialCointResponse, "Initial coint updated successfully") +} + +func (h *InitialCointHandler) DeleteInitialCoint(c *fiber.Ctx) error { + id := c.Params("coin_id") + if id == "" { + return utils.GenericResponse(c, fiber.StatusBadRequest, "Coin ID is required") + } + + err := h.InitialCointService.DeleteInitialCoint(id) + if err != nil { + return utils.GenericResponse(c, fiber.StatusNotFound, err.Error()) + } + + return utils.GenericResponse(c, fiber.StatusOK, "Initial coint deleted successfully") +} diff --git a/internal/repositories/initialcoint_repo.go b/internal/repositories/initialcoint_repo.go new file mode 100644 index 0000000..0a64709 --- /dev/null +++ b/internal/repositories/initialcoint_repo.go @@ -0,0 +1,67 @@ +package repositories + +import ( + "fmt" + "github.com/pahmiudahgede/senggoldong/model" + "gorm.io/gorm" +) + +type InitialCointRepository interface { + CreateInitialCoint(coint *model.InitialCoint) error + FindInitialCointByID(id string) (*model.InitialCoint, error) + FindAllInitialCoints() ([]model.InitialCoint, error) + UpdateInitialCoint(id string, coint *model.InitialCoint) error + DeleteInitialCoint(id string) error +} + +type initialCointRepository struct { + DB *gorm.DB +} + +func NewInitialCointRepository(db *gorm.DB) InitialCointRepository { + return &initialCointRepository{DB: db} +} + +func (r *initialCointRepository) CreateInitialCoint(coint *model.InitialCoint) error { + if err := r.DB.Create(coint).Error; err != nil { + return fmt.Errorf("failed to create initial coint: %v", err) + } + return nil +} + +func (r *initialCointRepository) FindInitialCointByID(id string) (*model.InitialCoint, error) { + var coint model.InitialCoint + err := r.DB.Where("id = ?", id).First(&coint).Error + if err != nil { + if err == gorm.ErrRecordNotFound { + return nil, fmt.Errorf("initial coint with ID %s not found", id) + } + return nil, fmt.Errorf("failed to fetch initial coint by ID: %v", err) + } + return &coint, nil +} + +func (r *initialCointRepository) FindAllInitialCoints() ([]model.InitialCoint, error) { + var coints []model.InitialCoint + err := r.DB.Find(&coints).Error + if err != nil { + return nil, fmt.Errorf("failed to fetch initial coints: %v", err) + } + return coints, nil +} + +func (r *initialCointRepository) UpdateInitialCoint(id string, coint *model.InitialCoint) error { + err := r.DB.Model(&model.InitialCoint{}).Where("id = ?", id).Updates(coint).Error + if err != nil { + return fmt.Errorf("failed to update initial coint: %v", err) + } + return nil +} + +func (r *initialCointRepository) DeleteInitialCoint(id string) error { + result := r.DB.Delete(&model.InitialCoint{}, "id = ?", id) + if result.Error != nil { + return fmt.Errorf("failed to delete initial coint: %v", result.Error) + } + return nil +} diff --git a/internal/services/initialcoint_service.go b/internal/services/initialcoint_service.go new file mode 100644 index 0000000..73bb599 --- /dev/null +++ b/internal/services/initialcoint_service.go @@ -0,0 +1,299 @@ +package services + +import ( + "fmt" + "time" + + "github.com/pahmiudahgede/senggoldong/dto" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/model" + "github.com/pahmiudahgede/senggoldong/utils" +) + +type InitialCointService interface { + CreateInitialCoint(request dto.RequestInitialCointDTO) (*dto.ReponseInitialCointDTO, error) + GetAllInitialCoints() ([]dto.ReponseInitialCointDTO, error) + GetInitialCointByID(id string) (*dto.ReponseInitialCointDTO, error) + UpdateInitialCoint(id string, request dto.RequestInitialCointDTO) (*dto.ReponseInitialCointDTO, error) + DeleteInitialCoint(id string) error +} + +type initialCointService struct { + InitialCointRepo repositories.InitialCointRepository +} + +func NewInitialCointService(repo repositories.InitialCointRepository) InitialCointService { + return &initialCointService{InitialCointRepo: repo} +} + +func (s *initialCointService) CreateInitialCoint(request dto.RequestInitialCointDTO) (*dto.ReponseInitialCointDTO, error) { + + errors, valid := request.ValidateCointInput() + if !valid { + return nil, fmt.Errorf("validation error: %v", errors) + } + + coint := model.InitialCoint{ + CoinName: request.CoinName, + ValuePerUnit: request.ValuePerUnit, + } + if err := s.InitialCointRepo.CreateInitialCoint(&coint); err != nil { + return nil, fmt.Errorf("failed to create initial coint: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(coint.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(coint.UpdatedAt) + + responseDTO := &dto.ReponseInitialCointDTO{ + ID: coint.ID, + CoinName: coint.CoinName, + ValuePerUnit: coint.ValuePerUnit, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + cacheKey := fmt.Sprintf("initialcoint:%s", coint.ID) + cacheData := map[string]interface{}{ + "data": responseDTO, + } + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching new initial coint: %v\n", err) + } + + err := s.updateAllCointCache() + if err != nil { + return nil, fmt.Errorf("error updating all initial coint cache: %v", err) + } + + return responseDTO, nil +} + +func (s *initialCointService) GetAllInitialCoints() ([]dto.ReponseInitialCointDTO, error) { + var cointsDTO []dto.ReponseInitialCointDTO + cacheKey := "initialcoints:all" + + cachedData, err := utils.GetJSONData(cacheKey) + if err != nil { + fmt.Printf("Error fetching cache for initialcoints: %v\n", err) + } + + if cachedData != nil { + if data, ok := cachedData["data"].([]interface{}); ok { + for _, item := range data { + if cointData, ok := item.(map[string]interface{}); ok { + + if coinID, ok := cointData["coin_id"].(string); ok { + if coinName, ok := cointData["coin_name"].(string); ok { + if valuePerUnit, ok := cointData["value_perunit"].(float64); ok { + if createdAt, ok := cointData["createdAt"].(string); ok { + if updatedAt, ok := cointData["updatedAt"].(string); ok { + + cointsDTO = append(cointsDTO, dto.ReponseInitialCointDTO{ + ID: coinID, + CoinName: coinName, + ValuePerUnit: valuePerUnit, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + } + } + } + } + } + } + return cointsDTO, nil + } + } + + records, err := s.InitialCointRepo.FindAllInitialCoints() + if err != nil { + return nil, fmt.Errorf("failed to fetch initial coints from database: %v", err) + } + + if len(records) == 0 { + return cointsDTO, nil + } + + for _, record := range records { + createdAt, _ := utils.FormatDateToIndonesianFormat(record.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(record.UpdatedAt) + + cointsDTO = append(cointsDTO, dto.ReponseInitialCointDTO{ + ID: record.ID, + CoinName: record.CoinName, + ValuePerUnit: record.ValuePerUnit, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + cacheData := map[string]interface{}{ + "data": cointsDTO, + } + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching all initial coints: %v\n", err) + } + + return cointsDTO, nil +} + +func (s *initialCointService) GetInitialCointByID(id string) (*dto.ReponseInitialCointDTO, error) { + cacheKey := fmt.Sprintf("initialcoint:%s", id) + cachedData, err := utils.GetJSONData(cacheKey) + if err == nil && cachedData != nil { + + if data, ok := cachedData["data"].(map[string]interface{}); ok { + + return &dto.ReponseInitialCointDTO{ + ID: data["coin_id"].(string), + CoinName: data["coin_name"].(string), + ValuePerUnit: data["value_perunit"].(float64), + CreatedAt: data["createdAt"].(string), + UpdatedAt: data["updatedAt"].(string), + }, nil + } else { + return nil, fmt.Errorf("error: cache data is not in the expected format for coin ID %s", id) + } + } + + coint, err := s.InitialCointRepo.FindInitialCointByID(id) + if err != nil { + return nil, fmt.Errorf("failed to fetch initial coint by ID %s: %v", id, err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(coint.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(coint.UpdatedAt) + + cointDTO := &dto.ReponseInitialCointDTO{ + ID: coint.ID, + CoinName: coint.CoinName, + ValuePerUnit: coint.ValuePerUnit, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + cacheData := map[string]interface{}{ + "data": cointDTO, + } + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching initial coint by ID: %v\n", err) + } + + return cointDTO, nil +} + +func (s *initialCointService) UpdateInitialCoint(id string, request dto.RequestInitialCointDTO) (*dto.ReponseInitialCointDTO, error) { + + coint, err := s.InitialCointRepo.FindInitialCointByID(id) + if err != nil { + return nil, fmt.Errorf("initial coint with ID %s not found", id) + } + + coint.CoinName = request.CoinName + coint.ValuePerUnit = request.ValuePerUnit + + if err := s.InitialCointRepo.UpdateInitialCoint(id, coint); err != nil { + return nil, fmt.Errorf("failed to update initial coint: %v", err) + } + + createdAt, _ := utils.FormatDateToIndonesianFormat(coint.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(coint.UpdatedAt) + + cointDTO := &dto.ReponseInitialCointDTO{ + ID: coint.ID, + CoinName: coint.CoinName, + ValuePerUnit: coint.ValuePerUnit, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } + + cacheKey := fmt.Sprintf("initialcoint:%s", id) + cacheData := map[string]interface{}{ + "data": cointDTO, + } + if err := utils.SetJSONData(cacheKey, cacheData, time.Hour*24); err != nil { + fmt.Printf("Error caching updated initial coint: %v\n", err) + } + + allCoints, err := s.InitialCointRepo.FindAllInitialCoints() + if err != nil { + return nil, fmt.Errorf("failed to fetch all initial coints from database: %v", err) + } + + var cointsDTO []dto.ReponseInitialCointDTO + for _, record := range allCoints { + createdAt, _ := utils.FormatDateToIndonesianFormat(record.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(record.UpdatedAt) + + cointsDTO = append(cointsDTO, dto.ReponseInitialCointDTO{ + ID: record.ID, + CoinName: record.CoinName, + ValuePerUnit: record.ValuePerUnit, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + cacheAllKey := "initialcoints:all" + cacheAllData := map[string]interface{}{ + "data": cointsDTO, + } + if err := utils.SetJSONData(cacheAllKey, cacheAllData, time.Hour*24); err != nil { + fmt.Printf("Error caching all initial coints: %v\n", err) + } + + return cointDTO, nil +} + +func (s *initialCointService) DeleteInitialCoint(id string) error { + + coint, err := s.InitialCointRepo.FindInitialCointByID(id) + if err != nil { + return fmt.Errorf("initial coint with ID %s not found", id) + } + + if err := s.InitialCointRepo.DeleteInitialCoint(id); err != nil { + return fmt.Errorf("failed to delete initial coint: %v", err) + } + + cacheKey := fmt.Sprintf("initialcoint:%s", coint.ID) + if err := utils.DeleteData(cacheKey); err != nil { + fmt.Printf("Error deleting cache for initial coint: %v\n", err) + } + + return s.updateAllCointCache() +} + +func (s *initialCointService) updateAllCointCache() error { + + records, err := s.InitialCointRepo.FindAllInitialCoints() + if err != nil { + return fmt.Errorf("failed to fetch all initial coints from database: %v", err) + } + + var cointsDTO []dto.ReponseInitialCointDTO + for _, record := range records { + createdAt, _ := utils.FormatDateToIndonesianFormat(record.CreatedAt) + updatedAt, _ := utils.FormatDateToIndonesianFormat(record.UpdatedAt) + + cointsDTO = append(cointsDTO, dto.ReponseInitialCointDTO{ + ID: record.ID, + CoinName: record.CoinName, + ValuePerUnit: record.ValuePerUnit, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + }) + } + + cacheAllKey := "initialcoints:all" + cacheAllData := map[string]interface{}{ + "data": cointsDTO, + } + if err := utils.SetJSONData(cacheAllKey, cacheAllData, time.Hour*24); err != nil { + fmt.Printf("Error caching all initial coints: %v\n", err) + return err + } + + return nil +} diff --git a/model/initialcoint_model.go b/model/initialcoint_model.go new file mode 100644 index 0000000..9f0f77a --- /dev/null +++ b/model/initialcoint_model.go @@ -0,0 +1,11 @@ +package model + +import "time" + +type InitialCoint struct { + ID string `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()" json:"id"` + CoinName string `gorm:"not null" json:"coin_name"` + ValuePerUnit float64 `gorm:"not null" json:"value_perunit"` + CreatedAt time.Time `gorm:"default:current_timestamp" json:"createdAt"` + UpdatedAt time.Time `gorm:"default:current_timestamp" json:"updatedAt"` +} diff --git a/presentation/initialcoint_route.go b/presentation/initialcoint_route.go new file mode 100644 index 0000000..9ef0dd5 --- /dev/null +++ b/presentation/initialcoint_route.go @@ -0,0 +1,27 @@ +package presentation + +import ( + "github.com/gofiber/fiber/v2" + "github.com/pahmiudahgede/senggoldong/config" + "github.com/pahmiudahgede/senggoldong/internal/handler" + "github.com/pahmiudahgede/senggoldong/internal/repositories" + "github.com/pahmiudahgede/senggoldong/internal/services" + "github.com/pahmiudahgede/senggoldong/middleware" + "github.com/pahmiudahgede/senggoldong/utils" +) + +func InitialCointRoute(api fiber.Router) { + + initialCointRepo := repositories.NewInitialCointRepository(config.DB) + initialCointService := services.NewInitialCointService(initialCointRepo) + initialCointHandler := handler.NewInitialCointHandler(initialCointService) + + initialCoint := api.Group("/initialcoint") + initialCoint.Use(middleware.AuthMiddleware, middleware.RoleMiddleware(utils.RoleAdministrator)) + + initialCoint.Post("/create", initialCointHandler.CreateInitialCoint) + initialCoint.Get("/getall", initialCointHandler.GetAllInitialCoints) + initialCoint.Get("/get/:coin_id", initialCointHandler.GetInitialCointByID) + initialCoint.Put("/update/:coin_id", initialCointHandler.UpdateInitialCoint) + initialCoint.Delete("/delete/:coin_id", initialCointHandler.DeleteInitialCoint) +} diff --git a/router/setup_routes.go.go b/router/setup_routes.go.go index da34d32..8841ff7 100644 --- a/router/setup_routes.go.go +++ b/router/setup_routes.go.go @@ -18,4 +18,5 @@ func SetupRoutes(app *fiber.App) { presentation.AddressRouter(api) presentation.ArticleRouter(api) presentation.BannerRouter(api) + presentation.InitialCointRoute(api) } diff --git a/utils/redis_caching.go b/utils/redis_caching.go index e1f15d2..24e748e 100644 --- a/utils/redis_caching.go +++ b/utils/redis_caching.go @@ -81,6 +81,10 @@ func GetJSONData(key string) (map[string]interface{}, error) { return nil, err } + if data == nil { + return nil, fmt.Errorf("error: no data found for key %s", key) + } + return data, nil }